6. 设计 tokens 与视觉基线(双风格:ClassIsland 默认 / winisland 兼容)
本节是整轮重构的「视觉地基」。实现者必须先把 token 落到 packages/desktop-client/src/island/ 下的独立样式入口,再改任何两个岛的渲染文件。所有像素、颜色、字号、圆角、阴影、透明度必须从这里取值,禁止在 IslandApp.vue、CustomIslandRuntime.vue、ComponentNode.vue、ClassicNotifications.vue、LessonReminderContent.vue、custom-runtime.css 里写裸值。
6.0 双风格模型(必须先建,再谈 token)
外观契约新增/改为(packages/shared/src/types.ts 的 IslandAppearance 扩展,config.ts 的 normalizeIslandAppearance 同步):
| 字段 |
类型 |
默认 |
含义 |
证据 |
family |
'classisland' | 'winisland' |
新装 classisland;老用户升级为 winisland(见 §6.12 迁移规则) |
视觉家族 |
Lead 裁决第 1 条(parts-r/01-objective.md:60) |
theme |
'fluent' | 'classic' |
fluent |
仅 family='classisland' 有效;对应 ClassIsland 两套 XamlTheme |
repos/ClassIsland/ClassIsland/App.Services.xaml.cs:297-315 |
colorScheme |
'light' | 'dark' | 'system' |
system |
仅 family='classisland' 有效;跟随应用深浅色 |
repos/ClassIsland/ClassIsland/Models/Settings.cs:979-989、Services/ThemeService.cs:53-63 |
style |
'black' | 'glass' | 'tinted' |
black |
仅 family='winisland' 有效(现有语义不变) |
packages/shared/src/constants.ts:265-290,324-330 |
animations |
boolean |
true |
岛动画总开关 |
constants.ts:273;IslandApp.vue:249 |
speed |
number |
1(0.5–2) |
动画速度倍率(按比例缩放所有时长) |
constants.ts:274;IslandApp.vue:291 |
fontSize |
number |
13(11–20) |
base 字号,四档字号由它派生 |
constants.ts:272;IslandApp.vue:527 |
height / width |
number |
44 / 268 |
仅 winisland 的胶囊几何;classisland 下用于「信息行最小宽/高」约束(见 §6.5) |
constants.ts:266-267;IslandApp.vue:188-204 |
radius |
number |
20(8–32) |
winisland 胶囊基准半径;classisland 下取 8(范围 0–20),见 §6.4 |
constants.ts:268;IslandApp.vue:209-211 |
实现要求:
- 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:
data-family="classisland|winisland"、data-theme="fluent|classic"、data-color-scheme="light|dark"(winisland 恒为 dark)。禁止用 JS 拼内联样式切风格。
family='classisland' 时不使用 spring.ts / squircle.ts:几何用标准 border-radius + CSS transition;family='winisland' 时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,parts-r/01-objective.md:52)。
- 两套 family 的 token 不得混用:classisland 的静态值定义在
--ci-*;winisland 的调色板保持 --wn-*;运行时外观注入继续用 --island-*(两套共用这几个动态变量:--island-font/--island-radius/--island-accent/--island-dur/--island-anim)。
- Token 文件必须是单一来源:新建
packages/desktop-client/src/island/tokens.css,在 island.html 的样式链最前面 import(src/island/main.ts 已 import @classhelper/ui/tokens.css,见 main.ts:1),并由 IslandApp.vue 与 CustomIslandRuntime.vue 共同继承。禁止在 .vue 的 <style> 里重新定义全局 token。
- 主题切换动画只允许改颜色类 token,绝不覆盖圆角/间距/字号/动画 token(与主窗口纪律一致:
docs/dev/motion.md:125-126)。
6.1 变量分层与命名规范
| 层 |
前缀 |
归属 |
是否可被运行时覆盖 |
本重构动作 |
| 1 应用外壳 |
--ch-ui-*、--ch-*、--el-* |
packages/ui/src/tokens.css、theme.css、renderer/styles/index.css |
是(主窗口主题) |
KEEP,不改值;岛只读引用(如编辑器 UI) |
| 2 ClassIsland 岛静态 token |
--ci-* |
新增 src/island/tokens.css |
否(改值只能改源码) |
NEW |
| 3 岛运行时外观 |
--island-* |
IslandApp.vue:522-528、CustomIslandRuntime.vue:102-123 注入 |
是(设置页实时) |
KEEP 并扩展(补 --island-row-*) |
| 4 winisland 调色板 |
--wn-* |
classic-island.css:60-82(.island-card 内) |
是(data-style 切换) |
KEEP(仅 winisland 生效) |
| 5 组件方案 |
--custom-*、--resident-*、--row-gap |
custom-island.css、custom-runtime.css 由 scheme 注入 |
是 |
KEEP,值按 §6.5 改默认 |
命名冲突处理(必读):现有 LessonReminderContent.vue:133-137 已在组件作用域注入 --ci-speed、--ci-duration、--ci-title-duration、--ci-scale-duration、--ci-delay,ComponentNode.vue/custom-runtime.css 还使用 --ci-body-font。这些是组件级变量,与本节新增的静态 token 同前缀,容易误伤。处理办法:
- 新增静态 token 中禁止使用这 6 个名字;
- M5 阶段把
LessonReminderContent.vue 的 5 个变量重命名为 --ci-lesson-speed/-duration/-title-duration/-scale-duration/-delay,保留旧名作为 alias(var(--ci-lesson-duration, var(--ci-duration, 200ms)))一版后删除;
--ci-body-font 保留(custom 行基准字号),在 tokens.css 中定义为 var(--ci-fs-body)。
6.1.1 --ci-* 变量速查表(必须在 tokens.css 中一次性定义齐)
实现者按此表逐行写入,缺一个都会在第 7/10 部分的样式里解析为空、导致整条声明失效。
| 变量 |
默认(classisland 深色) |
用途 |
--ci-bg |
#202020 |
岛/行背景色(配合 --ci-opacity-bg) |
--ci-bg-message |
#202020 |
阅读消息/详情实底 |
--ci-fill-1 |
rgba(255,255,255,.06) |
行内浅底(列表行、chip) |
--ci-fill-2 |
rgba(255,255,255,.10) |
悬停底、按钮底 |
--ci-fill-3 |
rgba(255,255,255,.14) |
徽标底、强强调底 |
--ci-border |
rgba(255,255,255,.15) |
岛/行 1px 描边 |
--ci-border-strong |
rgba(255,255,255,.24) |
选中/拖拽目标描边 |
--ci-border-accent |
var(--ci-accent) |
强调边(遮罩边缘) |
--ci-divider |
rgba(255,255,255,.09) |
行内分隔线 |
--ci-focus-ring |
var(--ci-accent) |
焦点环颜色 |
--ci-text-1 |
#FFFFFF |
主文字 |
--ci-text-2 |
rgba(255,255,255,.855) |
次要文字 |
--ci-text-3 |
rgba(255,255,255,.773) |
辅助文字 |
--ci-text-4 |
rgba(255,255,255,.529) |
装饰文字 |
--ci-text-5 |
rgba(255,255,255,.365) |
禁用文字 |
--ci-text-on-accent |
#FFFFFF |
accent 底上文字 |
--ci-accent |
#00BFFF(运行时被 --island-accent 覆盖) |
强调色 |
--ci-accent-soft |
accent @10% |
强调淡底 |
--ci-accent-soft-strong |
accent @14% |
强调淡底加强 |
--ci-state-urgent |
#FF453A |
紧急 |
--ci-state-call |
#FF9F0A |
叫人 |
--ci-state-homework |
var(--ci-accent) |
作业 |
--ci-state-ok |
#228B22 |
正常/优 |
--ci-warning |
#FFA500 |
警告 |
--ci-danger |
#CD5C5C |
危险/错误 |
--ci-tint-urgent |
径向 rgba(255,69,58,.22) |
紧急内层染色 |
--ci-tint-call |
径向 rgba(255,159,10,.20) |
叫人内层染色 |
--ci-progress-h |
3px |
行内进度条高度 |
--ci-progress-track |
rgba(255,255,255,.15) |
进度条轨道 |
--ci-progress-fill |
var(--ci-accent) |
进度条填充 |
--ci-progress-radius |
0 |
进度条圆角 |
--ci-mask-fill |
var(--ci-accent) |
斜切遮罩填充 |
--ci-mask-text |
#FFFFFF |
遮罩文字 |
--ci-radius-island |
8px |
岛圆角(0–20) |
--ci-radius-row |
8px |
单行圆角(分体) |
--ci-radius-card |
8px |
详情卡/气泡圆角 |
--ci-radius-control |
4px |
控件/列表行圆角 |
--ci-radius-pill |
999px |
胶囊/徽标圆角 |
--ci-row-h |
40px |
行高 |
--ci-row-pad-x |
12px |
行内左右内边距 |
--ci-row-min-w |
20px |
行内容最小宽 |
--ci-row-gap |
5px |
行间距(可调 0–150) |
--ci-comp-gap |
12px |
相邻组件间距(单侧 6) |
--ci-shadow-pad |
10px |
窗口/阴影留白 |
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,.28) |
岛投影 |
--ci-shadow-mask |
0 8px 32px rgba(0,0,0,.4) |
提醒时投影 |
--ci-shadow-popup |
0 8px 16px rgba(0,0,0,.45) |
岛内弹层投影 |
--ci-opacity-bg |
0.5 |
背景不透明度 |
--ci-opacity-faded |
0.05 |
指针避让淡化 |
--ci-fs-secondary |
14px(派生) |
次要字号 |
--ci-fs-body |
16px(派生) |
正文字号 |
--ci-fs-emphasized |
18px(派生) |
强调字号 |
--ci-fs-large |
20px(派生) |
大号字号 |
--ci-fw-regular |
400 |
常规字重 |
--ci-fw-medium |
500 |
中等字重 |
--ci-fw-bold |
700 |
粗字重 |
--ci-font |
HarmonyOS 栈 |
正文字体族 |
--ci-z-mask |
2 |
遮罩层 |
--ci-z-mask-text |
3 |
遮罩文字层 |
--ci-z-popup |
4 |
岛内弹层 |
--ci-z-drag-ghost |
10000 |
编辑器拖拽幽灵 |
--ci-dur-instant |
90ms |
高频反馈 |
--ci-dur-fast |
100ms |
淡出/短过渡 |
--ci-dur-base |
200ms |
默认过渡 |
--ci-dur-slow |
300ms |
行宽(Fluent) |
--ci-dur-overshoot |
550ms |
行宽(Classic) |
--ci-dur-long |
750ms |
遮罩缩放 |
--ci-ease-out |
cubic-bezier(.16,1,.3,1) |
ClassHelper 主缓动 |
--ci-ease-fl-1 |
cubic-bezier(.65,0,.35,1) |
Fluent 行宽 |
--ci-ease-fl-2 |
cubic-bezier(.25,1,.5,1) |
遮罩文字/缩放 |
--ci-ease-fl-3 |
cubic-bezier(.5,0,.75,0) |
遮罩退出 |
--ci-ease-overshoot |
cubic-bezier(0,1,.5,1.1) |
Classic 行宽过冲 |
--ci-ease-quartic-out |
cubic-bezier(.16,1,.3,1)【推断】 |
区段扫入 |
--ci-ease-quadratic-in |
cubic-bezier(.11,0,.5,0)【推断】 |
区段收拢 |
--ci-ease-sine-in |
cubic-bezier(.12,0,.39,0)【推断】 |
覆盖层淡入 |
--ci-dur-mask-region |
360ms |
单个斜切区段 |
--ci-dur-mask-stagger |
120ms |
区段错峰 |
--ci-delay-mask-text |
260ms |
遮罩文字延迟 |
--ci-dur-mask-text |
250ms |
遮罩文字淡入 |
--ci-dur-mask-scale |
750ms |
遮罩文字缩放 |
--ci-delay-mask |
600ms |
遮罩投影延迟 |
--ci-dur-mask-shadow |
100ms |
遮罩投影/背景淡入 |
--ci-dur-mask-exit |
200ms |
遮罩退出 |
6.2 颜色 token 表
6.2.1 背景与表面
| Token |
ClassIsland 目标值(family=classisland) |
ClassHelper 现状(变量/值) |
迁移策略 |
--ci-bg |
深色:#202020 @ opacity 0.5(Fluent 用 SolidBackgroundFillColorSecondaryBrush,Classic 用 SolidBackgroundFillColorBaseBrush);浅色:#F3F3F3 @ 0.5 |
--wn-bg:#000000(black)/rgba(32,32,36,0.804)(glass)/#0b0b0f(tinted),见 classic-island.css:60-82;custom:--wn-bg: #20202080(custom-runtime.css:241-245) |
NEW --ci-bg;classisland 下替换 .island-card 背景;winisland 保留 --wn-bg;custom 在 classisland family 下把方案默认色从 #000000 改为 #202020(island-layout.ts:246),并叠加 0.5 不透明度 |
--ci-bg-message |
#202020(不透明,阅读消息/详情时用实底) |
.message-surface { --wn-bg:#202020 }(classic-island.css:84-86、custom-runtime.css:253-255) |
KEEP 值,改名 --ci-bg-message 作为语义 token;winisland 继续用 .message-surface 覆盖 --wn-bg |
--ci-bg-elevated |
rgba(255,255,255,0.06)(行内 chip/进度底/列表行底) |
rgba(255,255,255,0.045)(.list-row,classic-island.css:389)、rgba(255,255,255,0.12)(.row-badge、.pill-type)、#ffffff0d/#ffffff1a(custom) |
CHANGE:classisland 统一到 6%/10%/14% 三档:--ci-fill-1/2/3 |
--ci-fill-1/2/3 |
6% / 10% / 14% 白(深色);4% / 8% / 12% 黑(浅色) |
同上散落 |
NEW |
--ci-mask-fill |
强调色(AccentFillColorDefaultBrush,ClassIsland 提醒遮罩填充) |
当前无(ClassHelper 消息用胶囊/详情卡,无遮罩) |
NEW,classisland fixed 消息气泡与 lesson 提醒共用 |
--ci-tint-urgent |
rgba(255,69,58,0.22) 径向 |
.urgent .tint 同值(classic-island.css:168-171) |
KEEP 值,归入 classisland 状态色 |
--ci-tint-call |
rgba(255,159,10,0.20) 径向 |
.call .tint 同值(:173-176) |
KEEP 值 |
--ci-tint-tinted |
accent 30% → transparent 135° 渐变 |
[data-style='tinted'] .tint(:178-185) |
KEEP(仅 winisland tinted) |
--ci-bg-hover |
rgba(255,255,255,0.10)(classisland 行/按钮 hover) |
--wn-surface-hover: rgba(255,255,255,0.18) / #ffffff1a |
CHANGE:classisland 用 10%,winisland 保留 18% |
6.2.2 描边
| Token |
ClassIsland 目标值 |
ClassHelper 现状 |
迁移策略 |
--ci-border |
1px rgba(255,255,255,0.15)(深色)/ rgba(0,0,0,0.08)(浅色) |
--wn-border: rgba(255,255,255,0.118)(classic-island.css:62);glass 0.157(:74);custom inset 0 0 0 1px #ffffff1e(custom-island.css:13) |
NEW;classisland 行容器 border:1px solid var(--ci-border);winisland 保留描边画在 SVG path 上(stroke-width:1,:110-113) |
--ci-border-strong |
rgba(255,255,255,0.24)(选中/拖拽目标) |
#ffffff2b(custom-island.css:171)、.node-editing:hover #ffffff28(ComponentNode.vue:432) |
NEW,统一 24% |
--ci-border-accent |
accent 100%(遮罩/强提醒强调边) |
--state-color 混合描边(classic-island.css:156-159,62% 状态色) |
CHANGE:classisland 用 accent 纯色;winisland 保留混合规则 |
--ci-divider |
rgba(255,255,255,0.09)(行内分隔) |
--wn-divider: rgba(255,255,255,0.09)(:68) |
KEEP 值,语义化改名 |
--ci-focus-ring |
2px accent,outline-offset:2px(设置/编辑器)+ -3px(岛内按钮) |
#8dafff/#adc4ff/#85aaff 三处不同(custom-island.css:65-68、custom-runtime.css:190-193、ComponentNode.vue:394) |
CHANGE:classisland 统一 2px solid var(--ci-accent);winisland 保留原色 |
6.2.3 文字阶梯
| Token |
ClassIsland 目标值(深色) |
ClassHelper 现状 |
迁移策略 |
--ci-text-1 |
#FFFFFF(100%) |
--wn-text-1: rgba(255,255,255,0.92)(classic-island.css:63) |
CHANGE:classisland 用 100%(ClassIsland 深层级 255 alpha,见 repos/ClassIsland/ClassIsland.Core/Helpers/UI/ControlColorHelper.cs:24-33);winisland 保留 0.92 |
--ci-text-2 |
rgba(255,255,255,0.855) |
--wn-text-2: 0.72(:64) |
CHANGE:classisland 85.5%;winisland 72% |
--ci-text-3 |
rgba(255,255,255,0.773) |
--wn-text-3: 0.58(:65) |
CHANGE:classisland 77.3%;winisland 58% |
--ci-text-4 |
rgba(255,255,255,0.529) |
无(classic 用 0.34 的 chevron,:317) |
NEW |
--ci-text-5 |
rgba(255,255,255,0.365) |
#ffffff60(custom-island.css:88)、#ffffff80(custom-runtime.css:145) |
NEW |
--ci-text-on-accent |
#FFFFFF(深色遮罩上) |
#fff(.title,classic-island.css:628)、#0b0b0f(.solid-btn,:711) |
NEW,遮罩/强提醒文字统一白;winisland solid-btn 保留深色字 |
| 浅色方案 |
#1B1B1B / rgba(0,0,0,0.855) / 0.773 / 0.529 / 0.365 |
岛当前恒为深色 |
NEW:classisland family 支持浅色(§6.10) |
字号必须同时遵守 ISLAND_TYPE_SCALE(winisland)与四档阶梯(classisland),见 §6.3。
6.2.4 强调色与状态色
| Token |
ClassIsland 目标值 |
ClassHelper 现状 |
迁移策略 |
--ci-accent |
ClassIsland 默认 #00BFFF(DeepSkyBlue,repos/ClassIsland/ClassIsland/Models/Settings.cs:35);运行时由 appearance.accent 覆盖 |
DEFAULT_ISLAND_APPEARANCE.accent = '#0a84ff'(constants.ts:271);IslandApp.vue:107 注入 |
KEEP 用户值;family='classisland' 且用户从未改过 accent(仍等于旧默认 #0a84ff)时迁移为 #00BFFF;否则保留。--island-accent 继续作为运行时变量 |
--ci-accent-soft |
accent @ 10%(ClassIsland CustomizedAccentBarBackground1Brush 运行时 = accent@10%,repos/ClassIsland/ClassIsland/Services/ThemeService.cs:66-74) |
color-mix(in srgb, var(--island-accent) 20%/22%/26%, transparent)(classic-island.css:375,524,545) |
CHANGE:classisland 统一 10%/14% 两档 --ci-accent-soft、--ci-accent-soft-strong |
--ci-state-urgent |
#FF453A |
#ff453a(classic-island.css:170,260,514,551) |
KEEP 值,改名 token |
--ci-state-call |
#FF9F0A |
#ff9f0a(:175,256,519,567) |
KEEP 值 |
--ci-state-homework |
accent |
accent(classic-island.css:263-265,417-419,523-526) |
KEEP |
--ci-state-ok |
#228B22(可选用天气/正常态) |
无 |
NEW(与 ClassIsland AQI 1 同值,见 Themes/LightTheme.axaml:10) |
--ci-danger |
#CD5C5C(IndianRed,ClassIsland 危险文字) |
#ffb4ab(课件错误,CoursewareTransfer.vue:127) |
KEEP 现状(课件)+新增 classisland --ci-danger |
--ci-warning |
#FFA500 |
无 |
NEW |
--ci-on-state |
#FFFFFF |
.badge-urgent #ff6961 等 |
KEEP winisland |
6.2.5 进度条与遮罩
| Token |
ClassIsland 目标值 |
ClassHelper 现状 |
迁移策略 |
--ci-progress-h |
3px(ClassIsland 行内进度条) |
2px(.lesson-progress,ComponentNode.vue:351)、2px(.ci-lesson-progress,LessonReminderContent.vue:312)、5px(课件,CoursewareTransfer.vue:111) |
CHANGE:classisland family 行内进度条 3px;课件保持 5px(不同组件,不受此 token 管) |
--ci-progress-track |
rgba(255,255,255,0.15) |
#ffffff24(ComponentNode.vue:352) |
CHANGE |
--ci-progress-fill |
var(--island-accent) |
accent(ComponentNode.vue:359、LessonReminderContent.vue:313) |
KEEP |
--ci-progress-radius |
0(ClassIsland 进度条直角) |
1px(ComponentNode.vue:354) |
CHANGE:classisland 0;winisland 保留 1px |
--ci-mask-fill |
accent(斜切遮罩填充,ClassIsland AccentFillColorDefaultBrush,repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:180) |
var(--island-accent, #60a5fa)(LessonReminderContent.vue:251) |
KEEP 语义,改用 --ci-mask-fill |
--ci-mask-text |
#FFFFFF(ClassIsland TextOnAccentFillColorPrimaryBrush) |
#fff |
KEEP |
6.2.6 Fluent / Classic 主题色差(classisland family 内部)
| Token |
Fluent(默认) |
Classic |
证据 |
| 行背景 |
SolidBackgroundFillColorSecondaryBrush(暗 #1C1C1C【推断】) |
SolidBackgroundFillColorBaseBrush(暗 #202020【推断】) |
repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:45;ClassicTheme/Styles.axaml:214 |
| 描边 |
1px ControlElevationBorderBrush |
无显式描边 |
FluentTheme:46-47 |
| 阴影 |
0 4 8 2 #48000000 |
常态无投影(提醒时 DropShadowEffect blur 8 opacity 1) |
FluentTheme:49;ClassicTheme:36-45,283-287 |
| 圆角 |
用 RadiusX(默认 8,0–20) |
直角(圆角 0) |
FluentTheme:48;ClassicTheme:46-59 |
| 遮罩动画 |
斜切分块 + 文字缩放 |
整块下压/上滑 |
见第 10 部分 |
实现:data-theme="fluent|classic" 只切换上表 5 项与第 10 部分的动画参数,不得切换字号/间距。
6.3 字体 token
| Token |
ClassIsland 目标值(classisland family) |
ClassHelper 现状 |
迁移策略 |
--ci-font |
'HarmonyOS Sans SC','Microsoft YaHei UI','Noto Sans CJK SC','Noto Sans SC','PingFang SC','Source Han Sans SC',SimHei,sans-serif(ClassIsland 字体栈,repos/ClassIsland/ClassIsland/Assets/Fonts/Fonts.axaml:3) |
-apple-system,'Segoe UI Variable Display','Segoe UI','Microsoft YaHei','PingFang SC','Helvetica Neue',sans-serif(classic-island.css:22-25) |
CHANGE:classisland family 用新栈;winisland 保留旧栈。不内置 TTF(许可与体积原因,HarmonyOS 字体许可见其 LICENSE.txt);若后续要内置,只加 @font-face 不改栈顺序 |
--ci-font-mono |
数字等宽:font-variant-numeric: tabular-nums |
已用(lesson-ending.css:11、LessonReminderContent.vue:226) |
KEEP,抽为工具类 .ci-num |
--ci-fs-secondary |
14px |
由 --secondary: (fontSize/13)*14(CustomIslandRuntime.vue:119)、.pill-sub 0.74×、small 12px(ComponentNode.vue:436) |
CHANGE:classisland 固定 4 档由 fontSize 按 14/16/18/20 比例派生(见下) |
--ci-fs-body |
16px |
--island-font 基准 13(winisland) |
CHANGE |
--ci-fs-emphasized |
18px |
--emphasized 默认 20(ComponentNode.vue:441) |
CHANGE |
--ci-fs-large |
20px |
--large 默认 28(ComponentNode.vue:444) |
CHANGE |
--ci-fw-regular/medium/bold |
400 / 500 / 700 |
600(.pill-title、.row-title、.badge)、700(.title)、600(.solid-btn) |
CHANGE:classisland 行文字 500、标题 700;winisland 保留 600/700 |
| 行高 |
body 1.25、说明 1.4(【推断】按 ClassIsland 40px 行高与 16px 字号比例) |
1.15(.pill-title/.pill-sub)、1.4(.content-text) |
CHANGE:classisland 1.25;winisland 保留 1.15 |
appearance.fontSize |
11–20,classisland 下表示 body 基准(默认 16);winisland 保持 13(基准字号) |
同一字段,默认 13 |
CHANGE 语义:按 family 解释;迁移时 winisland 用户不变,classisland 新装默认 16 |
classisland 四档派生公式(必须逐字实现):
这与现有 custom 的派生方式同构(CustomIslandRuntime.vue:119-122 用 /13 基准),只是基准从 13 换成 16。winisland 继续使用 ISLAND_TYPE_SCALE(constants.ts:354-367):title 1.08 / body 0.95 / meta 0.78 / badge 0.76 / pillTitle 0.92 / pillSub 0.74。
6.4 圆角 token
| Token |
classisland 目标 |
winisland 现状 |
custom 现状 |
迁移策略 |
--ci-radius-island |
8px(0–20 可调;appearance.radius 复用该范围) |
20px(8–32;squircle 基准半径) |
scheme.radius 默认 8 |
NEW;classisland 用标准 border-radius,不生成 squircle path |
--ci-radius-row |
8px(分体行与整体行同值) |
— |
— |
NEW |
--ci-radius-card |
8px(详情卡/气泡) |
展开卡 = min(48×radius/20, w/2, h/2)(IslandApp.vue:209-211) |
min(appearance.radius, w/2, h/2)(CustomIslandRuntime.vue:129-133) |
NEW |
--ci-radius-control |
4px(按钮/列表行/徽标内控件) |
calc(font×0.5)(.list-row、.row-icon) |
6px/8px 混用(custom-island.css:79,126) |
CHANGE:classisland 4px;winisland 保留比例派生 |
--ci-radius-pill |
999px |
999px(.pill-type/.badge/.ghost-btn) |
7px 圆点 |
KEEP |
--ci-radius-progress |
0 |
1px |
1px |
CHANGE(见 §6.2.5) |
| squircle 参数 |
不使用 |
指数 4.2、每角 14 段(squircle.ts:21,24) |
同 winisland(共用 squirclePath) |
KEEP(仅 winisland/custom) |
| 节点自定义圆角 |
默认 8(ClassIsland RadiusX) |
— |
islandNode().appearance.cornerRadius = 16(island-layout.ts:211) |
CHANGE:新默认 8;旧配置保留 16(无损迁移,不强制改) |
6.5 几何与间距 token
| Token |
classisland 目标 |
ClassHelper 现状/变量 |
迁移策略 |
行高 --ci-row-h |
40px(fixed 气泡同样恒 40,不随内容增高) |
--resident-height: 40、min-height: max(40px, font×2.5)(custom-runtime.css:77,110,113);winisland 胶囊高 appearance.height 默认 44 |
NEW;classisland 行高固定 40(不随字号/内容变;正文最多 2 行、超出省略,不得撑高行——见第 7 部分) |
行内边距 --ci-row-pad-x |
12px |
--resident-padding/padding 0 12px(custom-runtime.css:76);winisland padding: 0 calc(font×0.78) |
NEW;classisland 固定 12 |
行最小宽 --ci-row-min-w |
20px |
无 |
NEW(对应 ClassIsland MinWidth=20,repos/ClassIsland/.../FluentTheme/Styles.axaml:109) |
行间距 --ci-row-gap |
5px(0–150 可调) |
scheme.spacing 默认 5(island-layout.ts:251)、.layout-rows gap: var(--custom-row-gap) |
KEEP 值,classisland 下用 --ci-row-gap 驱动 --custom-row-gap |
组件间距 --ci-comp-gap |
相邻 12px(单侧 6px) |
--row-gap 默认 12(scheme/row gap,island-layout.ts:169,236;custom-runtime.css:117) |
KEEP;classisland 组件 padding 单侧 = calc(var(--row-gap)/2) 即 6px |
窗口/阴影留白 --ci-shadow-pad |
10px |
ISLAND_SHADOW_PAD = 10(constants.ts:351) |
KEEP |
自动布置容器 --ci-window-max |
960×640(custom 常驻画布) |
windowBox():min(960, area.width) × min(640, max(60, area.height-marginY))(main/island.ts:1521-1528) |
KEEP |
| 固定包围盒(fixed) |
宽 = min(工作区宽, 960)、高 = 40 + 2×pad(pad 默认 20,覆盖阴影);行内容在窗口内按停靠对齐并在超出时截断/省略;消息到达不改变窗口尺寸 |
BBOX_SIZE 取最大形态 + 20(island.ts:108-112,1543-1547) |
CHANGE:classisland 下包围盒 = 行宽 + 2×10;行宽变化时窗口在隐藏帧改尺寸(保留"不可见时改大小"策略);单条气泡恒 40px、不因内容扩窗,maxCardHeight 只用于 custom 多条列表与强提醒窗口 |
| 胶囊几何 |
不适用 |
width 268 / height 44(constants.ts:266-267),ISLAND_SIZE_DELTA(:344-348)、ISLAND_SLIVER_WIDTH 6(:336)、sliver height max(12,min(28,h-22))(IslandApp.vue:196) |
KEEP(仅 winisland) |
| 课程提醒条 |
高 40、宽 max-content、最小 300、最大 800 |
同(LessonReminderContent.vue:196-208) |
KEEP |
| 课件卡 |
424×132 |
COURSEWARE_ISLAND_SIZE(shared/src/courseware.ts:4)、padding 17/24、radius 24(CoursewareTransfer.vue:71-72) |
KEEP(独立组件,不套 classisland 行) |
| 编辑器预览行 |
40px(与真岛一致) |
.preview-row min-height:44px(island-editor.css:207)、padding 12、背景 #202024(:193-201) |
CHANGE:44→40,背景改 --ci-bg |
| 图标/徽标 |
下课前秒数徽标 30×30 圆形 |
.lesson-ending-badge 30×30(lesson-ending.css:3-5) |
KEEP |
6.6 阴影 token
| Token |
classisland 目标 |
ClassHelper 现状 |
迁移策略 |
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,0.28)(即 ClassIsland 0 4 8 2 #48000000) |
无(winisland 用 filter: drop-shadow(0 2px 3px rgba(0,0,0,.11)),classic-island.css:123,148-153) |
NEW:classisland 用 box-shadow 挂行容器(矩形 + 8px 圆角),禁止 filter: drop-shadow(会闪帧,classic-island.css:114-120) |
--ci-shadow-mask |
0 8px 32px rgba(0,0,0,0.4)(提醒时) |
无 |
NEW |
--ci-shadow-popup |
0 8px 16px rgba(0,0,0,0.45)(深色) |
--ch-shadow-flyout(index.css:53) |
NEW(岛内弹层用) |
--ci-shadow-custom |
内描边 + 小投影 |
inset 0 0 0 1px #ffffff1e, 0 2px 3px #0000001c(custom-island.css:12-14) |
KEEP(custom 行);classisland family 下改 inset 0 0 0 1px var(--ci-border), 0 4px 8px 2px rgba(0,0,0,.28) |
| 投影常驻原则 |
只改参数、不增删 filter/box-shadow 声明 |
已实现(classic-island.css:114-123) |
KEEP |
6.7 透明与不透明度 token
| Token |
classisland 目标 |
ClassHelper 现状/公式 |
迁移策略 |
--ci-opacity-bg |
0.5(行背景不透明度,与 ClassIsland 默认 Opacity=0.5 一致) |
scheme.opacity 默认 0.5(island-layout.ts:248);winisland appearance.opacity 默认 1 |
KEEP 语义分开:背景不透明度(0.5)≠ 窗口不透明度(1)。classisland 下 --ci-opacity-bg=scheme.opacity;winisland 无背景层不透明度概念 |
| 窗口不透明度 |
1;阅读消息时强制 1 |
displayOpacity:readingMessage ? 1 : appearance.opacity(main/island.ts:233-240) |
KEEP |
| 淡化比例 |
0.05 |
CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05(shared/src/island-layout.ts:35),用于 main/island.ts:1441 |
KEEP |
| 玻璃/毛玻璃 |
classisland 不使用 |
--wn-bg: rgba(32,32,36,0.804)(classic-island.css:73);不采样桌面模糊(文档 winisland-design-tokens.md:23) |
KEEP(winisland only) |
| 消息实底 |
1.0 不透明 #202020 |
.message-surface(见 §6.2.1) |
KEEP |
| 遮罩文字层 |
accent 上白字 100% |
同 |
KEEP |
| 禁用动画 |
不涉及透明度 |
`--island-anim: 0 |
1(IslandApp.vue:527);animations=false时所有transition/animation` 应为 0 或直接落位 |
6.8 层级(z-index)token
| 层 |
classisland 目标 |
ClassHelper 现状 |
迁移策略 |
| 岛根层 |
0 |
.island-root/.resident-window |
KEEP |
| 形状/背景层 |
0 |
.shape(classic-island.css:103-124)、svg.shape |
KEEP |
| 内容层 |
1 |
.content/.resident-page |
KEEP |
| 遮罩层(消息/提醒) |
2 |
.ci-mask(LessonReminderContent.vue:246-253)、.tint |
NEW 统一:--ci-z-mask:2 |
| 遮罩文字层 |
3 |
.ci-mask-title(:254-268) |
NEW:--ci-z-mask-text:3 |
| 弹层/下拉(岛内) |
4 |
无 |
NEW |
| 编辑器拖拽幽灵 |
10000 |
.drag-ghost z-index:10000(island-editor.css:331-334)、.ch-guide 10000 |
KEEP |
| 强提醒窗口 |
独立 BrowserWindow,alwaysOnTop + 'screen-saver' |
岛窗口同级别(island.ts:514) |
NEW:效果窗口层级与岛同级或更高,见第 8 部分 |
| 岛窗口层级 |
screen-saver(置顶) |
setAlwaysOnTop(alwaysOnTop,'screen-saver')(island.ts:514) |
KEEP |
6.9 DPI 与缩放 token
| 规则 |
要求 |
证据 |
| 逻辑单位 |
岛内全部用 CSS px = Electron DIP;不手动乘 devicePixelRatio |
winisland-design-tokens.md:53、01-objective.md:86 |
| 阴影留白 |
10 CSS px(窗口比岛大 20) |
ISLAND_SHADOW_PAD = 10(constants.ts:351);island.ts:1621-1636 |
| 画布上限 |
custom 常驻窗 ≤ 960×640(或工作区高 − marginY) |
island.ts:1521-1528 |
| 尺寸最小钳制 |
自定义窗口 ≥ 40 宽/24 高;可用画布 ≥ 40 |
island.ts:389-390、CustomIslandRuntime.vue:262-263 |
| 方案缩放 |
scale 0.75–2.5(custom 常驻);实际 fit.scale = min(scheme.scale, availW/naturalW, availH/naturalH) |
island-layout.ts:371-375;CustomIslandRuntime.vue:72-80 |
| 字号范围 |
11–20(appearance.fontSize);classisland 派生 4 档后仍受「行高 40」约束,溢出用省略而非撑行 |
constants.ts:298;§7 组件规格 |
| 主窗口缩放 |
设置页/编辑器沿用 --ch-font-scale(1 / 关怀 1.15),不影响岛 |
renderer/styles/index.css:103,973-979 |
| DPI 断言 |
截图/像素验收按 devicePixelRatio 换算基准图;125%/150% 下 ±1px 容差 |
01-objective.md:70 |
| 屏幕定位 |
全部用工作区 workArea + 锚点 + marginX/marginY;左右边距只在左/右停靠生效,主停靠中线不受影响 |
winisland-design-tokens.md:26;island.ts:1615-1648 |
6.10 深浅色 token
family='classisland' 必须同时支持深/浅色;family='winisland' 恒为深色(不随应用主题)。
| Token |
深色(默认) |
浅色 |
--ci-bg |
#202020 @0.5 |
#F3F3F3 @0.5 |
--ci-border |
rgba(255,255,255,0.15) |
rgba(0,0,0,0.08) |
--ci-text-1..5 |
白 100/85.5/77.3/52.9/36.5% |
黑 100/85.5/77.3/52.9/36.5% |
--ci-fill-1/2/3 |
白 6/10/14% |
黑 4/8/12% |
--ci-shadow-island |
0 4px 8px 2px rgba(0,0,0,0.28) |
0 4px 8px 2px rgba(0,0,0,0.18) |
--ci-progress-track |
白 15% |
黑 12% |
| 遮罩/强提醒 |
accent 底 + 白字 |
同(accent 底 + 白字) |
| 切换时机 |
主窗口 theme 变化 → 经 preload 下发 island:appearance,岛根 data-color-scheme 变;颜色过渡 200ms |
同 |
| 默认 |
colorScheme='system'(跟随应用;应用默认 Theme=2 深色,repos/ClassIsland/.../Settings.cs:32) |
— |
实现纪律:html.dark 式全局覆盖不适用于岛(岛是独立渲染入口,island.html 不继承主窗口 html.dark)。必须显式传 colorScheme 并在岛根节点切 data-color-scheme。切换只改颜色,几何/动效 token 保持不变(docs/dev/motion.md:125-130)。
6.11 两套风格差异总表(默认选择点)
| 维度 |
classisland(默认) |
winisland(兼容) |
| fixed 形态 |
信息行(多行,行高恒 40,组件横排,底部 3px 进度条);单条气泡也在 40px 行内、正文最多 2 行省略 |
胶囊 268×44 + 展开详情卡(ISLAND_SIZE_DELTA 展开 156×186) |
| 形状 |
标准 border-radius: 8px |
squircle 连续圆角(指数 4.2 / 14 段),胶囊满圆 h/2,展开 min(48r/20,…) |
| 形变 |
CSS transition(行宽/透明度;行高恒 40 不变) |
SpringValue 帧积分(stiffness 0.10 / damping 0.68 / 换向 35% / 上限 20% / snap 0.05px) |
| 内容切换 |
行内容淡入淡出 + 气泡两阶段 |
胶囊层 1-1.5p 退净、展开层 p² 进入 |
| 字号 |
14/16/18/20 四档(由 fontSize 派生) |
--island-font×排版系数(title 1.08/body .95/meta .78/…) |
| 字重 |
500 正文 / 700 标题 |
600 标题 / 700 大标题 |
| 颜色 |
深/浅色主题 + Fluent/Classic 两套;背景 0.5 透明 |
纯黑/毛玻璃/主题色渐变三套;恒深色 |
| 状态色 |
accent 遮罩 + 状态描边 |
红/橙/accent 内层微染 + 状态描边 |
| 进度条 |
3px、直角 |
1–2px、1px 圆角 |
| 主题切换 |
Fluent↔Classic / 深↔浅 各 200ms 颜色过渡 |
无主题概念,style 切换即时 |
| 设置项 |
theme+colorScheme 新增;height/width/radius 退化为约束 |
height/width/radius/style/idleSliver 全部保留 |
| 默认动画 |
见第 10 部分 classisland 列 |
见第 10 部分 winisland 列 |
6.12 配置迁移与兼容策略(token 相关部分)
config.json.island 增加 family、theme、colorScheme、winislandStyle(可选,默认为旧 style 的值)。
- 老用户升级:
family 缺失 → 判为老用户 → family='winisland',winislandStyle = 旧 style,theme/colorScheme 写默认值但忽略。新安装(无 config 或 island 缺失)→ classisland。
- 旧
style='black'|'glass'|'tinted' 保留字段(winisland 使用),同时写入 winislandStyle;classisland family 下忽略 style。
appearance.accent 保持用户值;仅当值等于旧默认 #0a84ff 且迁移到 classisland 时改 #00BFFF,并在迁移日志中记录。
appearance.fontSize:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。
scheme.radius/node.cornerRadius 不做强制改写(无损);classisland 渲染时若 radius 超过 20 则钳制到 20(appearance.radius 范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。
- 迁移必须幂等:
normalizeIslandAppearance() 对已有 family 的配置不得再次改值。
6.13 文件落点与实施顺序
| 步骤 |
文件 |
动作 |
| 1 |
packages/shared/src/types.ts |
扩展 IslandAppearance(family/theme/colorScheme/winislandStyle);导出 ISLAND_FAMILIES、ISLAND_THEMES、ISLAND_COLOR_SCHEMES |
| 2 |
packages/shared/src/constants.ts |
新增 classisland 默认值常量(DEFAULT_CLASSISLAND_FONT=16、ISLAND_CLASSISLAND_RANGES);把 ISLAND_SIZE_DELTA 标注为 winisland-only |
| 3 |
packages/shared/src/island-layout.ts |
CLASSISLAND_ISLAND_METRICS 增加 componentGap:12、progressHeight:3、rowMinWidth:20、shadowPad:10、字号四档保持现有值 |
| 4 |
新建 packages/desktop-client/src/island/tokens.css |
写 --ci-* 静态 token(含 [data-family]、[data-theme]、[data-color-scheme] 组合选择器) |
| 5 |
src/island/main.ts |
在 applyMotionTokens() 前 import ./tokens.css |
| 6 |
IslandApp.vue / CustomIslandRuntime.vue |
根元素挂 data-family/data-theme/data-color-scheme;style 注入补 --island-row-*/--ci-fs-* 派生值 |
| 7 |
classic-island.css / custom-island.css / custom-runtime.css / lesson-ending.css |
把硬编码颜色/尺寸改为引用 --ci-*(winisland 分支保留 --wn-*/--custom-*);[data-family='classisland'] 下覆盖布局 |
| 8 |
src/main/island.ts |
下发 family/theme/colorScheme;窗口包围盒按 classisland 行宽计算(保留固定包围盒与"不可见时改尺寸") |
| 9 |
设置页/编辑器 |
IslandSettingsView.vue 增加风格/主题/深浅色;island-editor.css 预览行 44→40、背景 token 化 |
| 10 |
docs/dev/winisland-design-tokens.md、docs/app/island.md |
标注双风格与默认值,新增 docs/dev/island-refactor.md 的 token 章 |
6.14 常见错误与自查清单
实现者必须主动规避以下 13 类错误(每一条都对应真实会出现的问题):
- 把
--ci-* 写进主窗口的 renderer/styles/index.css:岛是独立渲染入口,主窗口的 :root 不共享;写错位置会导致岛内变量解析为空、整条 transition 简写失效(docs/dev/motion.md:58-59 已记录该坑)。
- 漏掉
island.html 的样式引入顺序:tokens.css 必须在 classic-island.css/custom-*.css 之前 import,否则后者的 var() 在解析时找不到值。
- 用
html.dark 给岛做浅色:岛不加载主窗口的 <html class="dark">,必须 data-color-scheme。
- 在 classisland family 里继续用 squircle:
squirclePath() 只在 winisland/custom 分支调用;classisland 用 border-radius: var(--ci-radius-island)。
- 把
appearance.opacity 同时当作背景不透明度:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。
- 给行容器加
backdrop-filter:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的 glass 里以半透明色模拟(docs/dev/winisland-design-tokens.md:23-24)。
- 用
filter: drop-shadow 做 classisland 行投影:会新建合成表面导致开合闪帧;classisland 用 box-shadow(classic-island.css:114-120)。
- 在 classisland 分支继续用
--wn-text-*:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。
- 字号派生写成固定四档:classisland 四档必须由
fontSize/16 比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。
- 把圆角钳制只写在 CSS:
aspect 变化时 appearance.radius 可能大于行高一半,必须在 JS 里也钳制到 min(radius, w/2, h/2)(现有 IslandApp.vue:334-341 已做,classisland 分支同样要做)。
- 迁移时无条件改用户设置:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
- 忘记给编辑器预览同步 token:
island-editor.css 的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。
- 把 ClassHelper 旧展开卡行为带进 classisland fixed:fixed 行高恒 40,气泡/遮罩/进度条都在行内完成,正文最多 2 行省略;不得写「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」,
maxCardHeight 只服务 custom 多条列表与强提醒窗口。
自查命令(无头执行,不启动 Electron):
6.15 验收标准
- 单一来源:
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island 只允许命中 tokens.css、classic-island.css 的 --wn-* 定义块、custom-*.css 的兼容块与 SVG 渐变;两个 .vue 文件内联样式里不得出现颜色字面量(--state-color 计算值除外)。
- 双风格隔离:切到
winisland 后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到 classisland 后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。
- 默认值:新安装
family='classisland'、theme='fluent'、colorScheme='system';旧 fixtures 升级后 family='winisland' 且 winislandStyle 等于旧 style。
- 字号:classisland 下把
fontSize 从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。
- 颜色:深/浅色各截一张基准图,
--ci-text-1..5、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。
- Fluent/Classic:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
- 不透明度语义分离:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(
displayOpacity),关闭后回到 0.8。
- 层级:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵
z-index:10000 高于一切;岛窗口始终 screen-saver 层级。
- DPI:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
- 迁移幂等:对同一份配置连续运行
normalizeIslandAppearance() 三次,输出字节级一致;旧字段 residentEnabled、style 仍在且语义不变。
- 兼容别名:
--ci-lesson-* 重命名期间旧 --ci-duration 等变量仍生效(至少一个版本),组件视觉无变化。
- 无新依赖/无新框架:
package.json 依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。
- fixed 气泡恒定 40px:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口
bounds 不因该消息变化;maxCardHeight 只在 custom 多条列表/强提醒窗口路径被读取。
- reduce-motion 口径:系统开启
prefers-reduced-motion 时,岛动画仍由 appearance.animations/speed 决定(默认不覆盖);仅当 P2 选项 respectReducedMotion=true 时按 animations=false 落位。