Files
classhelper-island-refactor…/out/vol3-tokens与组件规格(06-07).md
T
ClassIsland Prompt Kit 22c1784bab feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
2026-11-12 21:59:58 +08:00

1225 lines
112 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` |
实现要求:
1. 岛根元素必须同时挂三个数据属性,CSS 用它们切 token 与几何:`data-family="classisland|winisland"`、`data-theme="fluent|classic"`、`data-color-scheme="light|dark"`(winisland 恒为 `dark`)。禁止用 JS 拼内联样式切风格。
2. `family='classisland'` 时**不使用** `spring.ts` / `squircle.ts`:几何用标准 `border-radius` + CSS transition;`family='winisland'` 时继续用弹簧形变与超椭圆路径(Lead 裁决第 4 条,`parts-r/01-objective.md:52`)。
3. 两套 family 的 token **不得混用**:classisland 的静态值定义在 `--ci-*`;winisland 的调色板保持 `--wn-*`;运行时外观注入继续用 `--island-*`(两套共用这几个动态变量:`--island-font`/`--island-radius`/`--island-accent`/`--island-dur`/`--island-anim`)。
4. 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。
5. 主题切换动画只允许改颜色类 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 四档派生公式(必须逐字实现)**:
```text
ratio = appearance.fontSize / 16 // 默认 16 → 1.0
--ci-fs-secondary = 14 * ratio
--ci-fs-body = 16 * ratio
--ci-fs-emphasized = 18 * ratio
--ci-fs-large = 20 * ratio
```
这与现有 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 或直接落位 | **KEEP 扩展**:classisland 新增 `--ci-anim: 0|1`,与 `--island-anim` 二选一由 family 决定 |
### 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 相关部分)
1. `config.json.island` 增加 `family`、`theme`、`colorScheme`、`winislandStyle`(可选,默认为旧 `style` 的值)。
2. **老用户升级**:`family` 缺失 → 判为老用户 → `family='winisland'`,`winislandStyle = 旧 style`,`theme/colorScheme` 写默认值但忽略。**新安装**(无 config 或 `island` 缺失)→ `classisland`。
3. 旧 `style='black'|'glass'|'tinted'` 保留字段(winisland 使用),同时写入 `winislandStyle`;classisland family 下忽略 `style`。
4. `appearance.accent` 保持用户值;仅当值等于旧默认 `#0a84ff` 且迁移到 classisland 时改 `#00BFFF`,并在迁移日志中记录。
5. `appearance.fontSize`:winisland 保持 13;迁移到 classisland 时若仍为 13 → 改为 16(避免 4 档过小),并记录。
6. `scheme.radius`/`node.cornerRadius` 不做强制改写(无损);classisland 渲染时若 `radius` 超过 20 则钳制到 20(`appearance.radius` 范围扩到 0–32 仅为兼容 winisland,classisland 侧钳制)。
7. 迁移必须幂等:`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 类错误(每一条都对应真实会出现的问题):
1. **把 `--ci-*` 写进主窗口的 `renderer/styles/index.css`**:岛是独立渲染入口,主窗口的 `:root` 不共享;写错位置会导致岛内变量解析为空、整条 `transition` 简写失效(`docs/dev/motion.md:58-59` 已记录该坑)。
2. **漏掉 `island.html` 的样式引入顺序**:`tokens.css` 必须在 `classic-island.css`/`custom-*.css` 之前 import,否则后者的 `var()` 在解析时找不到值。
3. **用 `html.dark` 给岛做浅色**:岛不加载主窗口的 `<html class="dark">`,必须 `data-color-scheme`。
4. **在 classisland family 里继续用 squircle**:`squirclePath()` 只在 winisland/custom 分支调用;classisland 用 `border-radius: var(--ci-radius-island)`。
5. **把 `appearance.opacity` 同时当作背景不透明度**:窗口不透明度(0.4–1,阅读消息强制 1)与背景不透明度(默认 0.5)是两个 token;混用会导致"消息看不清"或"背景太实"。
6. **给行容器加 `backdrop-filter`**:会模糊整块矩形并破坏逐像素透明;毛玻璃只在 winisland 的 `glass` 里以半透明色模拟(`docs/dev/winisland-design-tokens.md:23-24`)。
7. **用 `filter: drop-shadow` 做 classisland 行投影**:会新建合成表面导致开合闪帧;classisland 用 `box-shadow`(`classic-island.css:114-120`)。
8. **在 classisland 分支继续用 `--wn-text-*`**:winisland 的 0.92/0.72/0.58 与 classisland 的 100/85.5/77.3/52.9/36.5 不是一套;混用会让同一行文字出现两种灰。
9. **字号派生写成固定四档**:classisland 四档必须由 `fontSize/16` 比例派生;写死 14/16/18/20 会让设置页字号滑杆失效。
10. **把圆角钳制只写在 CSS**:`aspect` 变化时 `appearance.radius` 可能大于行高一半,必须在 JS 里也钳制到 `min(radius, w/2, h/2)`(现有 `IslandApp.vue:334-341` 已做,classisland 分支同样要做)。
11. **迁移时无条件改用户设置**:只有"仍等于旧默认值"的 accent/fontSize 才允许改;用户调过的值必须原样保留(§6.12)。
12. **忘记给编辑器预览同步 token**:`island-editor.css` 的预览行、背景、圆角必须与真岛一致,否则"预览一个样、上屏另一个样"。
13. **把 ClassHelper 旧展开卡行为带进 classisland fixed**:fixed 行高恒 40,气泡/遮罩/进度条都在行内完成,正文最多 2 行省略;**不得**写「行高随内容增高」「行高 200ms 过渡」「为气泡扩窗」,`maxCardHeight` 只服务 custom 多条列表与强提醒窗口。
自查命令(无头执行,不启动 Electron):
```bash
# 1) 颜色字面量只允许出现在 token/兼容定义处
grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island
# 2) 检查 classisland 分支没有误用 winisland 变量
grep -rn "\-\-wn-" packages/desktop-client/src/island/classic-island.css | head -40
# 3) token 是否齐全(每个 --ci- 使用处都能在 tokens.css 找到定义)
grep -rho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island | sort -u > /tmp/used.txt
grep -ho "\-\-ci-[a-z0-9-]*" packages/desktop-client/src/island/tokens.css | sort -u > /tmp/def.txt
comm -23 /tmp/used.txt /tmp/def.txt
# 4) 确认 package.json 无新增依赖
git diff packages/*/package.json package.json
```
### 6.15 验收标准
1. **单一来源**:`grep -rn "#[0-9a-fA-F]\{6\}" packages/desktop-client/src/island` 只允许命中 `tokens.css`、`classic-island.css` 的 `--wn-*` 定义块、`custom-*.css` 的兼容块与 SVG 渐变;两个 `.vue` 文件内联样式里不得出现颜色字面量(`--state-color` 计算值除外)。
2. **双风格隔离**:切到 `winisland` 后,岛仍显示 268×44 胶囊 + squircle + 弹簧开合,像素 diff 与重构前一致(±1px);切到 `classisland` 后无 squircle path、无弹簧形变,行高 40/圆角 8/进度条 3px。
3. **默认值**:新安装 `family='classisland'`、`theme='fluent'`、`colorScheme='system'`;旧 fixtures 升级后 `family='winisland'` 且 `winislandStyle` 等于旧 `style`。
4. **字号**:classisland 下把 `fontSize` 从 16 改到 20,四档变为 17.5/20/22.5/25,行高仍 40 且内容按第 7 部分规则省略;winisland 下改字号时全部文本按排版系数等比缩放。
5. **颜色**:深/浅色各截一张基准图,`--ci-text-1..5`、背景 0.5、描边 1px、进度条轨道/填充与 §6.2/§6.10 表逐项一致(颜色 ±2/255)。
6. **Fluent/Classic**:切换主题后,行背景色、描边有无、阴影、圆角(Fluent 8 / Classic 0)按 §6.2.6 变化,字号与间距不变。
7. **不透明度语义分离**:把行背景不透明度调到 0.2、窗口不透明度调到 0.8,两者独立生效;打开一条消息时窗口不透明度强制 1(`displayOpacity`),关闭后回到 0.8。
8. **层级**:消息遮罩在内容之上、遮罩文字在遮罩之上;编辑器拖拽幽灵 `z-index:10000` 高于一切;岛窗口始终 `screen-saver` 层级。
9. **DPI**:100%/125%/150%/200% 四档下,行高 40、圆角 8、字号逻辑值不变,无 0.5px 模糊;截图基准按 devicePixelRatio 换算后 ±1px。
10. **迁移幂等**:对同一份配置连续运行 `normalizeIslandAppearance()` 三次,输出字节级一致;旧字段 `residentEnabled`、`style` 仍在且语义不变。
11. **兼容别名**:`--ci-lesson-*` 重命名期间旧 `--ci-duration` 等变量仍生效(至少一个版本),组件视觉无变化。
12. **无新依赖/无新框架**:`package.json` 依赖零新增;token 只用 CSS 自定义属性,不引入 CSS-in-JS 或新 UI 库。
13. **fixed 气泡恒定 40px**:构造超长正文(>500 字)的消息,行高仍为 40、正文最多 2 行且以省略号收尾,窗口 `bounds` 不因该消息变化;`maxCardHeight` 只在 custom 多条列表/强提醒窗口路径被读取。
14. **reduce-motion 口径**:系统开启 `prefers-reduced-motion` 时,岛动画仍由 `appearance.animations`/`speed` 决定(默认不覆盖);仅当 P2 选项 `respectReducedMotion=true` 时按 `animations=false` 落位。
---
## 7. 岛结构与组件规格(ClassHelper 落地版)
本部分是「重构 ClassHelper 灵动岛」的视觉与组件规范。它**不是**新建工程的说明,而是对现有 `packages/desktop-client` 的改造指令:所有改动都必须落到既有文件(`src/island/*`、`src/main/island.ts`、`packages/shared/src/*`、`src/renderer/views/settings/*`)上,技术栈、构建方式、IPC 白名单、单窗口模型一律不变。
### 7.0 改造口径与文件级改动清单
**总口径(必须遵守)**:
1. 目标视觉**默认改为 ClassIsland 规格**(行高 40、行内边距 12、行距 5、圆角 8、背景不透明度 0.5、字号 14/16/18/20、进度条 3px、描边 1px、阴影 `0 4 8 2 #48000000`);ClassHelper 的 WinIsland 经典外观**只作为 fixed 的兼容外观族保留**(`islandAppearance.family === 'winisland'`)。**新安装默认 `family='classisland'`;老用户升级保持 `winisland`,避免观感突变**。
2. **`family` 只影响 `fixed`**:`classisland` 下 fixed = ClassIsland 风格信息行(1 行 = 日期 + 课程表,行高 40、圆角 8、背景 0.5、底部 3px 进度条;消息在**固定 40px 行内**展开 ClassIsland 气泡,保留 ClassHelper 既有暂存/紧急/叫人/已读/去重规则);`winisland` 下 fixed = 旧"胶囊 + 详情卡"(零改动)。**`custom` 始终使用 ClassIsland 规格,不提供 winisland 回退**(custom 现有 rows/nodes 结构本就接近 ClassIsland,只做细化与像素对齐,见 7.2、7.4)。
3. 不新增第二个窗口、不新增框架、不改服务端:`LayoutIslandApp.vue` 仍是唯一渲染入口,`main/island.ts` 仍是唯一窗口持有者(`packages/desktop-client/src/island/LayoutIslandApp.vue:1-76`)。
4. 消息/提醒/课程提醒的业务语义(优先级、去重、上课暂存、紧急展开、已读回执)归第 8 部分;本部分只规定它们**在岛上的容器、尺寸、颜色、动画**。
5. 所有像素值以本文与第 6 部分 tokens 为准;实现时**不得**再保留"ClassIsland 用 border-radius、ClassHelper 用 squircle"这种分歧——`fixed` 由 `appearance.family` 决定走哪套,`custom` 固定走 ClassIsland 套;代码里两套都在,但同一时刻只启用一套。
**必须改动的文件(按改动类型)**:
| 文件 | 改动类型 | 内容 |
|---|---|---|
| `packages/shared/src/constants.ts` | 扩展 | 新增 `ISLAND_FAMILIES` + `ISLAND_FAMILY_LABELS`;`DEFAULT_ISLAND_APPEARANCE` 增加 `family: 'classisland'`;新增 ClassIsland 规格常量(行高/内边距/行距/圆角/进度条/描边/阴影/字号)供渲染与主进程共用 |
| `packages/shared/src/island-layout.ts` | 扩展 | 组件 `settings` 增补 ClassIsland 语义字段(见 7.4);`appearance` 增补 `customMargin`(可选);`normalizeIslandLayouts()` 增加 `family` 迁移(老用户 → winisland)与规范化范围 |
| `packages/desktop-client/src/main/island.ts` | 改造 | `normalizeAppearance` 接受 `family`;`windowBox` 的 fixed/custom 包围盒按 7.1.2 调整;`updateResidentPointer` 的淡出比例沿用 shared 常量;新增 `island:report-layout-metrics` 承载"内容最大高" |
| `packages/desktop-client/src/island/IslandApp.vue` | 改造 | 增加 `data-family="classisland"` 分支:fixed 改为"信息行 + 行内气泡"(见 7.3.2);`winisland` 分支保持原样 |
| `packages/desktop-client/src/island/CustomIslandRuntime.vue` | 改造 | custom 常驻卡改为 ClassIsland 行容器(背景/圆角/内边距/行距/进度条/淡出);`classisland` 分支的 `border-radius` 与行背景 |
| `packages/desktop-client/src/island/ComponentNode.vue` | 改造 | 组件内边距 6 → 0(改由行 padding 承担);字号 token 改为 ClassIsland 的 14/16/18/20;新增 `schedule` 的 ClassIsland 折叠规则与进度条 3px;新增 countdown 的 ClassIsland 文案 |
| `packages/desktop-client/src/island/WeatherBrief.vue` | 改造 | 按 ClassIsland 天气简报的图标/数值排版(见 7.4.3) |
| `packages/desktop-client/src/island/classic-island.css` | 扩展 | 新增 `[data-family='winisland']` 下的三套 `data-style` 变量组保持现值;`classisland` 的行/气泡样式写进 `custom-runtime.css` |
| `packages/desktop-client/src/island/custom-runtime.css` | 改造 | 行/背景/进度条/淡出/编辑器选中框的 ClassIsland 规格 |
| `packages/desktop-client/src/island/custom-island.css` | 改造 | `.custom-card` 的 ClassIsland 背景/圆角/行距变量 |
| `packages/desktop-client/src/island/LessonReminderContent.vue` | 复用 | 其 5 段斜切遮罩算法**必须提取为共享遮罩组件**,供通知遮罩复用(见 7.2.6) |
| `packages/desktop-client/src/renderer/views/settings/IslandSettingsView.vue` | 改造 | 新增"外观族"下拉(绑定 `appearance.family`,默认 `classisland`,仅 `layout='fixed'` 可见);原"视觉风格"下拉仅在 `fixed+winisland` 下可见;`custom` 下两项都隐藏;圆角/行距/行高滑块范围按 ClassIsland 对齐 |
| `packages/desktop-client/src/renderer/views/settings/IslandEditorView.vue` + `island-editor.css` + `IslandEditorNode.vue` | 改造 | 选中框改 1.5px 虚线、行高与块级尺寸按 ClassIsland(见 7.5) |
| `packages/desktop-client/src/renderer/components/IslandComponentSettings.vue` | 改造 | 组件设置项按 7.4 的字段表补充(隐藏规则、宽度、对齐、边距、字体覆盖) |
**不得改动**:`main.ts` 的零依赖策略(`packages/desktop-client/src/island/main.ts:6-11`)、`preload/island.ts` 的白名单结构(`:14-62`)、`spring.ts` 的弹簧参数(`spring.ts:26-30`)、`squircle.ts` 的超椭圆参数(`squircle.ts:21-24`)。
### 7.1 岛窗口与几何
#### 7.1.1 窗口创建参数(保持不变,只能增加)
沿用现有 `BrowserWindow` 配置(`packages/desktop-client/src/main/island.ts:480-512`),并逐条核对:
- `frame: false`、`transparent: true`、`resizable: false`、`movable: false`、`minimizable/maximizable/fullscreenable: false`(`:484-490`)。
- `skipTaskbar: true`、`hasShadow: false`(`:491-492`)——阴影由 CSS/SVG 画,不得用系统窗口阴影。
- `focusable: true` **必须保留**(`:502`):Windows 下关掉可激活性会丢 `mousedown`,岛点不动(`:493-501` 的实机结论)。"不抢焦点"靠 `showInactive()` + 收起时 `blur()` 实现(`applyFocusable`,`:1663-1672`)。
- `alwaysOnTop: true` + `setAlwaysOnTop(appearance.alwaysOnTop, 'screen-saver')` + `setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })`(`:503, 514-515`)。
- `type: 'toolbar'`、`setIgnoreMouseEvents(true, { forward: true })`(`:504, 527`)。
- `webPreferences`: `contextIsolation: true`、`sandbox: true`、`backgroundThrottling: false`、preload 指向 `island.js`(`:505-511`)。
- **禁止**:`win.setBackgroundMaterial()` / `vibrancy` / `backgroundMaterial`(`:523-524` 的历史结论;逐像素透明是硬约束)。**禁止** `setContentProtection`(岛不是隐私窗口)。
- 导航与新窗口一律拒绝(`:516-522`)。
#### 7.1.2 窗口几何:fixed(两种外观族)/ custom
**fixed 的两种口径(由 `appearance.family` 决定,不是同一个形态)**
`fixed` 只表示"岛按经典交互工作(收到消息才出现)",它的**外观**由 `family` 决定:
**A. `family='classisland'`(新安装默认):ClassIsland 风格信息行**
- 形态**重定义**:fixed 不再是小胶囊,而是**一条 ClassIsland 风格的信息行**——默认 1 行 = 日期 + 课程表,行高 **40**、圆角 **8**、背景不透明度 **0.5**、1px 描边、阴影 `0 4 8 2 #48000000`、底部 **3px** 进度条(课程进度 / 提醒倒计时)。
- 隐藏语义不变:无消息时仍按现有 `state.mode='hidden'` 隐藏窗口(或按 `idleSliver` 留细缝,`main/island.ts:1483-1496`);消息到来才显示行。
- 消息到来:**在同一行内、行高 40px 的固定容器里渲染 ClassIsland 风格气泡**——气泡不是"把行撑高",而是整块 40px 行换上遮罩层 + 内容层 + 进度层(见 7.2.8)。**行高恒为 40,禁止临时增高到 64 等**(`--ci-island-height: 40px` 不得被气泡改写)。保留 ClassHelper 既有业务规则(普通上课暂存、紧急立即展开、普通叫人可点、已读回执、按 id 去重),业务规则归第 8 部分,视觉归 7.2.8。
- 窗口几何(固定包围盒,开合时不动):
- 宽 = 行内容自然宽 + 2 × 12(行内边距)+ 2 × `ISLAND_SHADOW_PAD`(=10,`packages/shared/src/constants.ts:351`);
- **高恒为 40 + 2 × `ISLAND_SHADOW_PAD`**(无消息 / 有消息都一样;气泡在行内,不改变窗口高度);
- 只在"行宽跨档"、显示器/DPI 变化或"窗口不可见"时 `setBounds`(沿用 `applyWindowLayout` 的比较逻辑,`main/island.ts:1598-1612`)。
- 行宽/内容宽必须由渲染层测量后上报:`reportSize({ width, height })`(`CustomIslandRuntime.vue:255`)+ `setHitRect`(`IslandApp.vue:421-454`),主进程**不得**自行猜内容尺寸。
- 气泡宽度上限 **720**【推断:按 1080p 下 ClassIsland 岛常用宽度取整】;超宽时课程表按 `settings.count` 截断(7.4.5),气泡正文最多 2 行(7.2.8)。
- 鼠标交互:行上可点(展开/收起气泡、按钮);点行外空白 → 收起气泡(不改变 hidden 语义)。
**B. `family='winisland'`(老用户升级默认):胶囊 + 详情卡**
- 保留现有全部几何:`windowBox()` 普通模式取 `max(BBOX_SIZE, 当前形态 + 2*pad)`(`main/island.ts:1543-1548`);触摸模式取"当前形态 + 2*pad"并贴合岛体(`:1549-1552`)。
- 胶囊 = `appearance.width × height`(默认 268×44,`constants.ts:266-267`),展开/紧急/叫人 = 胶囊 + `ISLAND_SIZE_DELTA`(`:344-348`);岛本体尺寸由渲染层弹簧逼近(`IslandApp.vue:216-220, 238-264`)。
- 圆角 = `winisland` 规格(胶囊 `h/2`、展开卡 `min(48×radius/20, w/2, h/2)`),背景 = 纯黑/毛玻璃/主题色渐变三选一。
**custom(常驻):固定画布 + 内容自然测量(始终 ClassIsland 规格,与 `family` 无关)**
- **外观不走 `family`**:custom 无论 `family` 是 `classisland` 还是 `winisland`,都按 7.2 的 ClassIsland 行/背景/进度条/遮罩规格渲染;`family` 只决定 fixed 用哪种外观(7.3.1)。设置页在 `layout='custom'` 时不得展示/生效 `family` 与 `winisland` 色调选项。
- 现在的窗口画布已经是"固定画布":`windowBox()` 在 `residentEnabled` 时返回 `min(960, area.width) × min(640, max(60, area.height - marginY))`(`main/island.ts:1522-1529`),这与 ClassIsland 的固定包围盒思路一致,**保留**。
- 目标态改动:画布高度上限改为"内容上限"而不是固定 640——渲染进程上报 `reportLayoutMetrics({ contentMaxHeight })`(新增 IPC),主进程取 `min(area.height - marginY, contentMaxHeight + 2*pad)`;默认 `contentMaxHeight = 常态行数 × (40 + 行距) + 24`。理由:ClassIsland 的岛高度只由行决定(`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:916`),640 会让屏幕上方出现一块"看不见但可能吃掉点击"的余量(虽然命中已按 hitRect 控制,但窗口越大越依赖 hitRect 的准确性)。
- `available`(渲染层可用尺寸)保持现值算法:`innerWidth/innerHeight - 2*ISLAND_SHADOW_PAD - 4`(`CustomIslandRuntime.vue:260-266`),最小值 40。
- 内容缩放:`fitted()` 取 `min(scheme.scale, available.width/natural.width, available.height/natural.height)`(`:72-80`),**保留**;`scheme.scale` 范围 0.75–2.5(`island-layout.ts:370-375`)。
- `customSize` 初值 424×80(`main/island.ts:261`)保留,仅作为首帧占位;首次 `reportSize` 后覆盖。
#### 7.1.3 锚点、停靠与多显示器
必须沿用现有算法(`main/island.ts:1331-1351`),逐条核对:
- 停靠位置 6 值:`top-center / top-left / top-right / bottom-center / bottom-left / bottom-right`(`constants.ts:305-312`)。
- 垂直:`vMode = position.startsWith('bottom') ? 'bottom' : 'top'`;`vValue = bottom ? area.y + area.height - marginY : area.y + marginY`(`:1341-1342`)。
- 水平:`hMode = endsWith('left') ? 'left' : endsWith('right') ? 'right' : 'center'`;`hValue = left ? area.x + marginX : right ? area.x + area.width - marginX : round(area.x + area.width/2)`(`:1343-1349`)。
- **居中停靠不受 `marginX` 影响**(`docs/dev/winisland-design-tokens.md:26`),只有左右停靠用水平边距。
- 锚点全部取整(`Math.round`),避免 0.5px 抖动(`:1325-1330`)。
- 窗口定位时窗口比岛大 `ISLAND_SHADOW_PAD`,因此要再偏移一个 pad,让**岛**的边/中心落在锚点上(`windowBounds`,`:1615-1648`):左停靠 `x = hValue - pad`、右停靠 `x = hValue + pad - width`、居中 `x = hValue - round(width/2)`;`y = bottom ? vValue + pad - height : vValue - pad`。
- 多显示器:`targetDisplay()` = `followCursorDisplay ? getDisplayNearestPoint(cursor) : getPrimaryDisplay()`(`:1354-1358`);换屏检测挂在既有 60ms 光标轮询上(`syncAnchorDisplay`,`:1370-1379`),**不得**新增定时器。
- DPI:Electron 的 `BrowserWindow.setBounds` 用 DIP,渲染层 CSS px 与 DIP 一一对应,**禁止**在渲染层再乘 `devicePixelRatio` 或 `zoomFactor`;`appearance` 里的宽高/边距全部是 DIP(`constants.ts:264-290` 的注释口径)。
- 目标态新增:ClassIsland 的"竖直安全区"(Fluent 20px / Classic 0,`repos/ClassIsland/ClassIsland/App.Services.xaml.cs:314`)在 ClassHelper 里以 **P2 / 可选**实现——仅当 `appearance.family === 'classisland'` 且设置项开启时,把锚点向屏幕内侧再推 `safe` 像素;默认 0,不得改变现有视觉位置。
#### 7.1.4 点击穿透、命中与触摸
这是岛最容易出 regress 的部分,**必须逐条保持**:
1. 窗口默认 `setIgnoreMouseEvents(true, { forward: true })`(`main/island.ts:527`)。
2. 渲染层只上报"进入岛体"(`setInteractive(true)`),**不上报关闭**;关闭由主进程按真实光标轮询决定(`IslandApp.vue:456-487` 的整段说明)。理由:两边都下发会让窗口在"接收/穿透"之间每几毫秒翻转,表现为"点了没反应"。
3. 命中矩形 `hitRect`:渲染层用 `.island-card`/`.resident-card` 的 `getBoundingClientRect()` 上报(`IslandApp.vue:421-454`、`CustomIslandRuntime.vue:200-213`),限流 50ms 且尾随发送一次;主进程按 60ms 兜底校正(`main/island.ts:1824-1836`)。
4. 触摸模式:渲染层按 `navigator.maxTouchPoints > 0` 上报 `setTouchMode(true)`(`IslandApp.vue:546-549`),主进程切到"窗口贴合岛体 + 始终接收输入"(`main/island.ts:1891-1899, 1795-1807`)。触摸模式的窗口**只增不减**,收小要等窗口隐藏后再做(`syncTouchLayout`,`:1560-1578`)。
5. 失焦宽限:`BLUR_GRACE_MS = 500`、触摸/编辑态 `BLUR_GRACE_TOUCH_MS = 1200`(`main/island.ts:2287, 2297`);blur 时若指针仍在岛上则忽略(`:565-578`)。
6. 常驻模式的指针避让淡化:`residentFaded` 由 `updateResidentPointer()` 计算(`:1851-1877`),触发条件包含"非编辑、非课程提醒、非展开、非触摸操作后 1s、未悬停铃铛",淡化比例 = `CLASSISLAND_ISLAND_METRICS.fadedOpacity = 0.05`(`island-layout.ts:35`)。
7. 编辑预览时**不创建额外桌面窗口**(`docs/app/island.md:47`),编辑态由 `island:set-editing` 控制 `setInteractive(true)`(`main/island.ts:1430-1432`)。
#### 7.1.5 窗口护栏(不得删减)
- 渲染进程心跳:渲染层每 5s `alive()`(`LayoutIslandApp.vue:36`),主进程 `RENDERER_ALIVE_TIMEOUT_MS = 20_000`(`main/island.ts:2315`),超时隐藏并重建窗口,重建限流 3 次 / 10 分钟(`:2317-2318`)。
- 淡入淡出:步进 0.16 / 0.2、16ms 一步(`fadeIn/fadeOut`,`:1674-1710`);`appearance.animations === false` 时直接落位。
- 窗口只在"尺寸/位置设置变化"或"不可见"时重排(`applyWindowLayout` 前先比较 4 个值,`:1598-1612`);**禁止**在开合动画中改窗口几何(`:1477-1479` 的注释)。
- `suspended`(被 custom 或课件覆盖时)必须停掉经典岛的测量与命中(`IslandApp.vue:238-246` 的 `syncTargets` 早退)。
### 7.2 行、背景、进度条、遮罩与淡出(ClassIsland 规格:custom 恒定 / fixed 默认)
#### 7.2.1 行容器 DOM(目标态)
`CustomIslandRuntime.vue` 的常态页与展开页都必须渲染成下面这棵树(现状见 `:396-504`,需按此重排):
```html
<div class="resident-window" data-anchor="left|center|right" data-bottom="true|false" data-family="classisland" :data-style="appearance.style">
<section class="custom-card resident-card island-card">
<svg class="shape" v-if="appearance.family === 'winisland'"><path :d="path" /></svg>
<!-- classisland:不用 squircle 形状层,圆角由 CSS border-radius 承担(见 7.2.2) -->
<span class="tint" aria-hidden="true"></span>
<div class="resident-page resident-compact-page">
<div class="resident-source resident-compact">
<div class="layout-rows" style="gap: var(--ci-line-gap)">
<section class="layout-row" data-row="main">
<div class="row-content">
<div class="row-components" style="gap: var(--ci-component-gap)">
<ComponentNode … />
</div>
</div>
</section>
</div>
<button class="resident-bell">…</button> <!-- 消息入口,见 7.4.12 -->
</div>
</div>
<div class="resident-page resident-detail-page">…</div>
<div class="resident-page resident-lesson-page">…</div>
</section>
</div>
```
强制要求:
- 行容器类名保持 `.layout-row` / `.row-content` / `.row-components`(现有 CSS 已依赖,`custom-island.css:145-164`、`custom-runtime.css:98-121`),**不要改名**;新增的规格通过 CSS 变量注入。
- `.layout-rows` 的 `gap` 从 `scheme.spacing` 改为 `var(--ci-line-gap)`(默认 5px,`island-layout.ts:34`),`scheme.spacing` 仅作为覆盖值(编辑器"行间距"仍是它)。
- 组件列表的 `gap` 从 `row.gap` 改为 `var(--ci-component-gap)`(默认 12px)。语义映射:ClassIsland 的相邻组件间距 = 两个 `Margin="6 0"` 之和 = 12(`repos/ClassIsland/ClassIsland/MainWindow.axaml:396`)。因此 `row.gap` 的默认值应从 12 改为 **12 并作为覆盖**(`island-layout.ts:343` 默认 8 → 改为 12),范围 0–64 保留。
#### 7.2.2 行的像素规格(ClassIsland 规格)
必须写死为 CSS 变量并在两个 CSS 文件里引用(`custom-runtime.css` 为主):
```css
.resident-window {
--ci-line-height: 40px; /* ClassIsland IslandContainerHeight,MainWindowLine.axaml:16 */
--ci-line-pad-x: 12px; /* GridContentRoot Margin="12 0",FluentTheme/Styles.axaml:110 */
--ci-line-gap: 5px; /* MainWindowLineVerticalMargin 默认 5,Models/Settings.cs:219 */
--ci-component-gap: 12px; /* 组件 Margin="6 0" ×2,MainWindow.axaml:396 */
--ci-radius-island: 8px; /* RadiusX 默认 8,Models/Settings.cs:183 */
--ci-island-opacity: 0.5; /* Opacity 默认 0.5,Models/Settings.cs:64 */
--ci-line-opacity: 1; /* 行整体不透明度默认 1.0,MainWindowLineSettings.cs:42 */
--ci-stroke-island-w: 1px; /* line-background BorderThickness=1,FluentTheme/Styles.axaml:47 */
--ci-shadow-island: 0 4px 8px 2px #48000000; /* FluentTheme/Styles.axaml:49 */
--ci-progress-height: 3px; /* 本轮裁决,所有进度条共用 */
--ci-fade-ratio: 0.05; /* CLASSISLAND_ISLAND_METRICS.fadedOpacity,island-layout.ts:35 */
}
```
行本体:
```css
.layout-row {
min-height: var(--ci-line-height);
padding: 0 var(--ci-line-pad-x); /* ClassIsland 的 12px 内边距搬到行上 */
box-sizing: border-box;
display: flex;
align-items: center;
opacity: var(--ci-line-opacity);
}
.row-components {
display: flex;
align-items: center;
gap: var(--ci-component-gap);
min-height: var(--ci-line-height);
}
```
**方案级覆盖**:第 5、13 部分在 scheme 上新增 `rowHeight` / `componentGap` / `showProgress` 三个字段(默认 40 / 12 / true)。渲染时按"scheme 覆盖 > 外观族默认"写入 CSS 变量:
```css
.resident-card {
--ci-line-height: v-bind('scheme.rowHeight ? scheme.rowHeight + "px" : "40px"');
--ci-component-gap: v-bind('scheme.componentGap + "px"');
}
```
`showProgress=false` 时所有行内进度条(课程进度、倒计时进度)隐藏;行距仍是 `--ci-line-gap`(默认 5,来自 `scheme.spacing`,`island-layout.ts:34`)。
背景(两种形态都要):
```css
/* 整块岛背景(separated=false) */
.resident-card {
background: var(--ci-bg-secondary); /* SolidBackgroundFillColorSecondaryBrush */
border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
border-radius: var(--ci-radius-island);
box-shadow: var(--ci-shadow-island);
opacity: var(--ci-island-opacity); /* 只作用背景,不作用文字 */
}
/* 分体(scheme.separated=true):每行一个独立胶囊 */
.resident-card.resident-separated { background: transparent; box-shadow: none; border: 0; padding: 0; }
.resident-separated .layout-row {
background: var(--ci-bg-secondary);
border: var(--ci-stroke-island-w) solid var(--ci-stroke-island);
border-radius: var(--ci-radius-island);
box-shadow: var(--ci-shadow-island);
opacity: var(--ci-island-opacity);
padding: 0 var(--ci-line-pad-x);
}
```
**硬性要求**:背景不透明度只允许作用在背景层;文字/图标必须 100% 不透明(ClassIsland 把 `Opacity` 绑在 `Border.line-background` 上,`repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:44`)。现有实现里卡片 `opacity` 由主进程 `win.setOpacity()` 统一控制(`main/island.ts:1440-1442`),**不得**把背景不透明度也塞进窗口 opacity——必须拆成"窗口 opacity(用户可调 0.4–1)+ 背景层 alpha(0.5)"两层。
#### 7.2.3 组件内边距的归零
现在 `ComponentNode` 自身有 `padding: 6px`(`ComponentNode.vue:295-301`),行 `.row-components` 的 `gap` 又是 `row.gap`,两者叠加会让实际间距变成 `gap + 12`。ClassIsland 是"组件 0 内边距 + 行 12px 内边距 + 组件 6/6 外边距"。
改造:
```css
.component-node { padding: 0; } /* 由行 padding 承担左右留白 */
.row-components { gap: var(--ci-component-gap); } /* 12px */
```
同时在 `ComponentNode` 上**移除** `overflow: hidden`(`ComponentNode.vue:296`)→ 改为 `overflow: visible`,否则倒计时进度环、天气预警告牌会被裁掉;裁切交给行(`clip-path` 或 `overflow: hidden` 挂在 `.layout-row`)。
#### 7.2.4 进度条
三条进度条统一 `3px`、`border-radius: 1.5px`、贴底、`transform-origin: left`:
| 位置 | 现状 | 目标 |
|---|---|---|
| 课程行内进度 | `.lesson-progress { height: 2px; background: #ffffff24 }`(`ComponentNode.vue:348-355`) | `height: var(--ci-progress-height)`;底色 `rgba(255,255,255,0.08)`;填充 `var(--ci-accent)` |
| 课程提醒卡进度 | `.ci-lesson-progress { height: 2px }`(`LessonReminderContent.vue:307-316`) | 同上,高度 3px |
| 通知遮罩进度(新增) | 无 | 同 7.2.6,贴在遮罩层底部 |
过渡:课程进度 `transition: transform 1s linear`(`ComponentNode.vue:361`)保留;提醒进度 `50ms linear`(`LessonReminderContent.vue:315`)保留;通知倒计时进度 `100ms linear`(`prefers-reduced-motion` 时 0)。
#### 7.2.5 淡出与透明度
- 整窗淡入淡出:主进程步进 0.16/0.2 × 16ms(`main/island.ts:1674-1710`)保留。
- 指针避让淡化:`residentFaded` → `win.setOpacity(displayOpacity × 0.05)`(`:1440-1442` + `island-layout.ts:35`)保留;**必须**保证展开态、编辑态、课程提醒期间不淡化(`:1864-1873`)。
- 行级淡出(新增,P2):ClassIsland 的 `IsLineFaded` 是 0.05(`repos/ClassIsland/ClassIsland/Controls/MainWindowLine.axaml:73-75`);ClassHelper 已有"整窗淡化",**默认不叠加行级淡化**,仅当 `pointerFadeReversed` 语义需要时用行级 opacity 过渡 100ms 实现。
#### 7.2.6 斜切遮罩(通知遮罩必须复用课程提醒的实现)
ClassIsland 的遮罩是 5 段斜切,参数为:区段权重 `[0, 0.1, 0.2, 0.4, 0.2, 0.1]`、60° 斜面、区段 360ms、错峰 120ms、开 `QuarticEaseOut` / 关 `QuadraticEaseIn`、文字 delay 260ms + 250ms 淡入 + scale 1.1→1.0 / 750ms(`repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200, 284-308`;行模板 `XamlThemes/FluentTheme/Styles.axaml:329-367`)。
**现状**:这套算法已经存在于 `LessonReminderContent.vue:59-102`,并且参数完全对得上——`edges = [0, 0.1, 0.3, 0.7, 0.9, 1]`(等价权重 0/0.1/0.2/0.4/0.2/0.1)、`offset = tan(30°) × 40`、`delays = open ? [0,90,120,90,0] : [120,90,0,90,120]`、`ease = 1-(1-p)^4` / `p²`、总时长 480ms、兜底 600ms。
**改造要求**:
1. 把 `LessonReminderContent.vue` 的遮罩部分抽成 `packages/desktop-client/src/island/SlantedMask.vue`(props:`width`、`height`、`open: boolean`、`accent: string`、`motion: boolean`、`speed: number`),`LessonReminderContent` 与新的通知遮罩共用。
2. 通知遮罩的 DOM 必须与 ClassIsland 对齐:
```html
<div class="ci-mask-layer" :class="{ open }">
<svg class="ci-mask" :viewBox="`0 0 ${width} 40`" preserveAspectRatio="none">
<polygon v-for="(p, i) in polygons" :key="i" :points="p" />
</svg>
<div class="ci-mask-title" :class="{ 'ci-title-in': open }">…通知标题…</div>
</div>
```
3. 遮罩填充色 = `var(--ci-accent-fill)`(ClassIsland 用 `AccentFillColorDefaultBrush`);文字色 = `var(--ci-text-on-accent)`。
4. `prefers-reduced-motion` 或 `appearance.animations === false` 时直接落位(`regions` 全部 0/1),不做逐帧插值(`LessonReminderContent.vue:77-80` 已有该分支,保留)。
5. **不得**给遮罩层加 `filter: blur()` 或 `backdrop-filter`(透明窗口下不可靠)。
#### 7.2.7 行/组件隐藏规则
ClassHelper 现有的 `advanced.hideRules` 只有三值:`in-class` / `no-message` / `no-class`(`island-layout.ts:101-106`,判定在 `ComponentNode.vue:108-120`)。目标是 ClassIsland 的规则集(`HideMode` 基本/高级、`HideOnClass`、`HideOnMaxWindow`、`HideOnFullscreen`、规则集编辑器,`repos/ClassIsland/ClassIsland/Models/Settings.cs:185, 418-505`)。
本轮落地口径:
- **保留**现有三值并保证语义不变(`in-class` ↔ 上课隐藏、`no-message` ↔ 无消息隐藏、`no-class` ↔ 无课程隐藏)。
- **新增**(v2 schema,见第 5 部分):`hideRules` 允许字符串扩展 `'max-window' | 'fullscreen'`,判定数据由主进程通过 `IslandState` 新增字段 `foreground: { maximized: boolean; fullscreen: boolean }` 下发(主进程已有前台窗口检测能力,`repos/ClassIsland/ClassIsland/MainWindow.axaml.cs:444-457` 为语义参考)。
- **行级**隐藏(ClassIsland 的 `MainWindowLineSettings.HideOnRule`)本轮只做"行整体可隐藏",P1;规则集编辑器(`RulesetControl`)不在本轮范围,P2。
- 编辑器必须能编辑 `hideRules`(现有三值)与 `align/width/maxWidth`;新增两项后要同步 `IslandComponentSettings.vue` 与 `normalizeIslandLayouts` 的白名单(`island-layout.ts:321-328`)。
#### 7.2.8 消息气泡(`family='classisland'` 的 fixed 模式,40px 行内)
**硬性约束:气泡不得增高行、不得增高窗口。** 行高恒 `40px`(`--ci-island-height: 40px` 不得被气泡改写),气泡的遮罩、内容、进度条全部在这个 40px 容器内完成。
DOM(叠在行内容之上,`position: absolute; inset: 0`):
```html
<div class="ci-row-shell"> <!-- height: 40px; position: relative -->
<div class="layout-row">…常态行组件…</div> <!-- 气泡展开时 opacity → 0(不卸载,保留宽度测量) -->
<div class="ci-row-bubble" v-if="mode==='expanded'"> <!-- inset: 0; border-radius: 8px; overflow: hidden -->
<SlantedMask class="ci-bubble-mask" :open="true" :width="rowWidth" :height="40" />
<div class="ci-bubble-row"> <!-- display:flex; align-items:center; height:40px; padding: 0 12px -->
<span class="ci-bubble-icon">…</span> <!-- 16×16 线性图标 -->
<span class="ci-bubble-badge">紧急</span> <!-- 可选;高 18、圆角 9、字号 11 -->
<div class="ci-bubble-text"> <!-- min-width:0; flex:1; display:flex; flex-direction:column; justify-content:center -->
<span class="ci-bubble-title">…单行省略…</span>
<span class="ci-bubble-body">…最多 2 行…</span>
</div>
<span class="ci-bubble-actions">…按钮…</span>
</div>
<div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
</div>
</div>
```
CSS 规格:
```css
.ci-row-bubble { position: absolute; inset: 0; border-radius: var(--ci-radius-island); overflow: hidden; }
.ci-bubble-text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 0; }
.ci-bubble-title {
font-size: var(--ci-fs-body); /* 16px */
font-weight: 600; line-height: 20px; /* 40px 行内两行:20 + 18 */
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ci-bubble-body {
font-size: var(--ci-fs-secondary); /* 14px */
line-height: 18px; color: var(--ci-text-2);
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; /* 正文最多 2 行,超出省略 */
}
.ci-bubble-progress {
position: absolute; left: 0; bottom: 0; width: 100%; height: var(--ci-progress-height);
background: var(--ci-accent-fill); transform-origin: left;
transition: transform 50ms linear;
}
```
**行内的排版预算**:标题 1 行(20px)+ 正文 2 行(18×2=36)已超过 40px,因此实际规则是——**标题占用 1 行时正文最多 1 行;无标题时正文最多 2 行**;两种都靠 `-webkit-line-clamp` + 固定行高把内容锁在 40px 内,**不得**用 `overflow: visible` 让文字溢出到行外。
**动画(严格按 ClassIsland 原版参数)**:
| 阶段 | 属性 | 时长 / 延迟 | 缓动 |
|---|---|---|---|
| 遮罩斜掠(开) | 5 段 `scaleX 0→1` | 每段 360ms,错峰 0/90/90/120/120ms | `cubic-bezier(0.165,0.84,0.44,1)`(QuarticEaseOut) |
| 遮罩斜掠(关) | 5 段 `scaleX 1→0` | 每段 360ms,反向错峰 | `cubic-bezier(0.55,0.085,0.68,0.53)`(QuadraticEaseIn) |
| 气泡文字淡入 | `opacity 0→1` | 250ms,delay 260ms | `cubic-bezier(0.25,1,0.5,1)` |
| 气泡文字缩放 | `scale 1.1→1.0` | 750ms,delay 260ms | `cubic-bezier(0.25,1,0.5,1)` |
| 气泡文字淡出 | `opacity 1→0` | **200ms** | 线性 |
| 文字缩回 | `scale 1.0→1.1` | **200ms** | `cubic-bezier(0.5,0,0.75,0)` |
| 常态行淡出 | `opacity 1→0` | 200ms | `SineEaseIn`(同 ClassIsland 的 `GridContentRoot` 过渡) |
| 进度条 | `scaleX` | 50ms | 线性 |
以上参数与 ClassIsland 完全一致(`repos/ClassIsland/ClassIsland/Controls/SlantedMaskControl.cs:177-200`;`XamlThemes/FluentTheme/Styles.axaml:329-367`)。
**Classic 主题(`appearance.theme='classic'`)**:遮罩改为 `translateY(-50px → 0)`、200ms `QuadraticEaseIn`;退出 `0 → +50px`、delay 100ms、200ms `QuadraticEaseOut`;文字 200ms delay / 100ms(`XamlThemes/ClassicTheme/Styles.axaml:265-331`)。
**禁止**:`islandListLayout()` 的高度再参与 fixed 气泡(它只用于 custom 详情页与 winisland 详情卡);把气泡做成"行下方新增一块 64px 高的区域"。
### 7.3 外观族:`family='classisland'`(默认)与 `family='winisland'`(兼容,仅 fixed)
**适用范围(硬性)**:`appearance.family` **只影响 `fixed` 模式的外观**;`custom` 始终按 ClassIsland 规格渲染(7.2、7.4),**不提供 winisland 回退**。
#### 7.3.1 数据与开关(字段名以第 5 部分为准)
**新增顶层字段 `appearance.family: 'classisland' | 'winisland'`**,与既有 `appearance.style`(`black/glass/tinted` 色调)**不是一个概念**:
- `family` 决定 **fixed** 长什么样、怎么排(信息行 vs 胶囊、圆角 8 vs squircle);对 `custom` 无意义;
- `style` 只在 `fixed + family='winisland'` 下有意义(纯黑/毛玻璃/主题色渐变),其余情况忽略。
落地规则:
1. `packages/shared/src/constants.ts` 新增 `ISLAND_FAMILIES = ['classisland','winisland'] as const` + `ISLAND_FAMILY_LABELS`(`classisland: 'ClassIsland(默认)'`、`winisland: 'WinIsland 经典'`);`ISLAND_STYLES`(`:324`)与 `ISLAND_STYLE_LABELS`(`:326-330`)**保持原样不动**。
2. `DEFAULT_ISLAND_APPEARANCE` 增加 `family: 'classisland'`(`:265-290`)——这是"新安装默认"。
3. `normalizeAppearance()` 接受 `family` 白名单(`main/island.ts:2244-2264`),非法值回退 `'classisland'`。
4. **升级迁移(必须与第 5 部分一致)**:旧配置没有 `family` 字段 → 迁移时写入 `family: 'winisland'`(老用户观感不变);新安装 → `'classisland'`。迁移函数放在 `packages/shared/src/island-layout.ts` 的 `normalizeIslandLayouts()` 里,与 `version` 一并处理。
5. 设置页拆分(`IslandSettingsView.vue:291-305` 现绑定 `style`):
- "外观族"下拉绑定 `appearance.family`,**仅在 `layout='fixed'` 时可见**;
- "视觉风格/色调"下拉仅在 `layout='fixed' && family='winisland'` 时可见;
- `layout='custom'` 时两者都隐藏(custom 恒 ClassIsland 规格),只保留圆角/行高/行距/字号等 Scheme 级设置;
- 切换必须即时生效并可回退(走既有 `appearance` 通道,`preload/island.ts:48-49`)。
6. 主题轴(`appearance.theme: 'fluent' | 'classic'`)由第 6 部分定义;本部分只要求"行背景色/描边/阴影/遮罩动画参数随 theme 切换",落点在 `custom-runtime.css` 的 `[data-theme='classic']` 覆盖组(custom 与 fixed-classisland 共用)。
#### 7.3.2 `family='classisland'`(默认,必须实现)
**fixed 模式下(重定义后)= 信息行 + 行内气泡**:
| 维度 | 规格 | 落点 |
|---|---|---|
| 形态 | **1 行信息行**(默认 日期 + 课程表),不是胶囊 | `IslandApp.vue` 新增 `classisland` 模板分支(见下方改造点) |
| 行高 | 40 | `--ci-line-height` |
| 行内边距 | 左右 12 | `--ci-line-pad-x` |
| 圆角 | 8(矩形圆角,不用 squircle) | `border-radius: var(--ci-radius-island)` |
| 背景 | 不透明度 0.5 + 1px 描边 + 阴影 `0 4 8 2 #48000000` | 7.2.2 |
| 进度条 | 底部 3px(课程进度 / 提醒倒计时) | 7.2.4 |
| 字号 | 14 / 16 / 18 / 20 | `--ci-fs-*` |
| 消息 | **在固定 40px 行内**渲染 ClassIsland 气泡:斜切遮罩(5 段)+ 标题 + 正文(最多 2 行省略)+ 底部 3px 倒计时进度条;**不增高行、不增高窗口** | 7.2.8 + 第 8 部分 |
| 空闲 | 保持 `hidden` / `idleSliver` 语义(不改成常驻) | `main/island.ts:1483-1496` |
| 形变 | **无高度形变**:行高恒 40;只有"行 ↔ 气泡"的内容层交叉淡入/遮罩斜掠;`radius` 恒 8 | `IslandApp.vue:216-264` 的 `classisland` 分支 |
**custom 模式下** = 现有 schemes/rows/nodes 模型升级,**始终 ClassIsland 规格**(7.2、7.4 全部适用),与 `family` 无关;课程提醒接管规则见 7.3.3。
**关键改造点(`IslandApp.vue`)**:现有 fixed 模板是"胶囊层 + 展开层 + 课程层"(`:606-678`),胶囊内容是"图标 + 标题 + 副标题 + chevron"(`:616-659`)。改造要求:
```html
<!-- family=classisland:fixed 信息行(固定 40px 高,气泡在行内叠加) -->
<div class="ci-row-shell" style="height: 40px">
<div class="layout-row"><div class="row-components">
<ComponentNode v-for="node in fixedRowNodes" … />
</div></div>
<div v-if="mode==='expanded'" class="ci-row-bubble" role="region">
<SlantedMask class="ci-row-mask" :open="true" :width="rowWidth" :height="40" />
<div class="ci-bubble-content">…标题 / 正文(2 行省略)/ 操作…</div>
<div class="ci-bubble-progress" :style="{ transform: `scaleX(${leftProgress})` }" />
</div>
</div>
```
- `fixedRowNodes` = 默认 `[islandNode('date','fixed-date'), islandNode('schedule','fixed-schedule')]`;来源可以新建一个内置 fixed 方案(与 custom 的 scheme 结构复用),**不要**把节点写死在模板里。
- `targetSize()` 增加 `classisland` 分支:**高度恒 40**,`{ width: mode==='expanded' ? max(rowNaturalWidth + 24, 420) : rowNaturalWidth + 24, height: 40 }`;**禁止**出现 `40 + bubbleHeight` 之类的增高分支。
- 展开/收起**不做高度弹簧**:`mode` 切换时只播"行内容 → 气泡"的遮罩斜掠与内容交叉淡入(7.2.8 的 360/120ms 与 260/250ms 参数);行高、窗口高全程不动。
- `family='winisland'` 时走原模板,逻辑零改动。
#### 7.3.3 fixed 与 custom 差异总表(含课程提醒接管)
先给出三种组合的**行为**差异总表(后续 7.3.4 细化 winisland、7.3.5 说明共享约束),不得自行发明第四种组合:
| 维度 | `fixed` + `family='classisland'`(默认) | `fixed` + `family='winisland'`(兼容) | `custom`(常驻,**恒 ClassIsland 规格,family 不适用**) |
|---|---|---|---|
| 常态 | 隐藏(或细缝);消息到来才显示 1 行信息行 | 隐藏;消息到来弹胶囊 | 常驻方案 `rows` |
| 行高 | 恒 40 | 胶囊 44 / 展开卡按内容 | `scheme.rowHeight`(默认 40) |
| 消息展示 | **固定 40px 行内**气泡(遮罩+标题+正文 2 行+3px 进度),**行不增高** | 胶囊 → 详情卡(高度按 `islandListLayout`),窗口高度随形态变 | 铃铛 + 详情页(`resident-detail-page`),不改成行内气泡 |
| 课程提醒 | **显示完整课程提醒条**:`LessonReminderContent` 渲染在 40px 行内(准备/上课/课间/下课前) | 显示课程提醒条(现有 `ci-fixed-lesson` 覆盖) | **不因 in-class/prepare/break 临时接管整岛**;课程进度由 `schedule` 组件展示;`ending` 只在课程名右侧显示蓝色圆形秒数徽标(`lesson-ending.css` 的 30×30);**不新增"临时接管一行"行为** |
| 课程进度 | 行底 3px 进度条(当前课程) | 课程提醒条底部 2px(改 3px,7.2.4) | `schedule` 组件内 3px 进度条 |
| 多消息 | 第 8 部分规则;气泡内最多 2 行正文,超出省略 | 列表模式(`islandListLayout`)| 详情页 `ClassicNotifications` list-mode |
| 空闲交互 | 鼠标移入不淡化(隐藏态);显示时按命中接收 | 指针避让淡化 0.05 | 指针避让淡化 0.05(`updateResidentPointer`) |
| 窗口几何 | 宽 = min(工作区宽, 960),高恒 40+2×pad(pad 默认 20) | 固定包围盒(最大形态 + 2×pad) | 固定画布(内容上限 + 2×pad) |
**custom 课程提醒的落地改动(必须做,且只做这些)**:
- `CustomIslandRuntime.vue:31-33` 的 `lesson` 计算改为 **恒 `null`**(不再让 prepare/on-class/break/in-class 触发 `resident-lesson` 页),并从模板移除/停用 `resident-lesson-page`(`:505-524`)——custom 不再整岛接管。
- 保留 `ending` 徽标:`ComponentNode.vue` 的 `.lesson-ending-badge`(`:261-266`,样式 `lesson-ending.css:1-16`)继续在课程名右侧显示 `-{remaining}`,尺寸 30×30、`#0a84ff` 底、白字 12px/600、等宽数字。
- **不得**新增 "custom 下临时接管一行显示课程提醒" 的行为(这是本轮明确排除的范围)。
#### 7.3.4 `family='winisland'`(兼容,仅 fixed,保持现有外观)
- 几何:胶囊 `width × height`(默认 268×44)、圆角 = `min(appearance.radius, h/2)`(`IslandApp.vue:206-212`)、展开卡半径 = `min(48 × radius/20, w/2, h/2)`。
- 形状:squircle 超椭圆(`squircle.ts:21-24`,指数 4.2、每角 14 段)。
- 底色:`appearance.style` 的 `black` / `glass` / `tinted` 三套 CSS 变量组(`classic-island.css:60-86`),用 `data-style` 属性表达(现有模板已写 `:data-style="appearance.style"`,`IslandApp.vue:587`、`CustomIslandRuntime.vue:348`)。
- 排版:`--island-font` 基础字号 + 系数(`ISLAND_TYPE_SCALE`,`constants.ts:354-367`),胶囊内边距 `0 font*0.78`、gap `font*0.62`(`classic-island.css:207-216`)。
- 阴影:只在展开态 `0 2px 3px rgba(0,0,0,.11)`,且**声明常驻只改参数**(`classic-island.css:110-124, 148-153`)。
- 交互:胶囊点击展开、点击卡片空白收起、失焦宽限 500ms——全部保持现状。
#### 7.3.5 三种组合的共享约束
无论 `layout` 与 `family` 如何组合都必须满足:
- 命中/穿透/触摸/失焦/心跳逻辑**完全不变**(7.1.4、7.1.5):family 只改 **fixed** 的渲染,不改输入、不影响 custom。
- 编辑器与设置页共用同一份 `islandLayout` 数据;`family` 不改变数据模型(除 7.4 的字段增补);`custom` 的 scheme/rows/nodes 在两套 family 下渲染完全一致。
- 所有组合的 `ComponentNode` 是**同一个组件**,规格共用 7.4;差异只在行容器、背景层与气泡容器。
- `prefers-reduced-motion` 与应用内"动画"开关同时生效;关闭动画时所有弹簧直接 `snap()`(`CustomIslandRuntime.vue:148-151`、`IslandApp.vue:249-256`)。
- 可回退:feature flag(`family`)必须在 M2/M3 阶段即可一键切回旧 fixed 渲染;`custom` 无回退档(始终 ClassIsland 规格),验收时 `fixed×2 family` 与 `custom` 三条路径都要跑(第 11、12 部分)。
### 7.4 组件逐个规格(11 内置 + 3 消息入口 + 课件覆盖)
统一约定(所有组件适用):
- 契约字段:`id / kind / settings / appearance / advanced / children`(`island-layout.ts:77-108`)。
- `appearance`:`resourceOverridingEnabled / secondaryFontSize / bodyFontSize / emphasizedFontSize / largeFontSize / customForegroundEnabled / foreground / customBackgroundEnabled / background / customBackgroundOpacityEnabled / backgroundOpacity / customCornerRadiusEnabled / cornerRadius / opacity`(`:61-76`)。
- `advanced`:`hideRules / align / width / maxWidth`(`:101-106`)。
- 默认值来源:`islandNode()`(`:173-216`),规范化边界在 `normalizeIslandLayouts`(`:257-403`)。
- 外观应用方式:`ComponentNode.style` 计算(`ComponentNode.vue:179-210`)——`opacity`、`width`、`maxWidth`、`alignSelf`(仅父级有宽度时)、`textAlign`、自定义前景/背景/圆角、字号覆盖、容器方向与 gap。
- 字体:`--secondary` 默认 14、`--emphasized` 默认 18、`--large` 默认 20(由 `CustomIslandRuntime.vue:119-122` 按 `appearance.fontSize/13` 比例算出)→ **改为直接使用 `--ci-fs-secondary/emphasized/large`(14/18/20)**,不再按字号比例缩放;只有 `appearance.fontSize` 作为"整岛缩放"时再乘系数。
- 图标:统一用 `ComponentIcon.vue` 的 24×24 线性图标(`ComponentIcon.vue:4-35`,`stroke-width 1.5`);ClassIsland 用 FluentSystemIcons,**不引入新图标字体**,按语义映射(见各组件)。
#### 7.4.1 `date` 日期
- 数据字段:仅 `settings` 常规字段(`text` 等未使用)。
- 渲染:`new Date(now).toLocaleDateString('zh-CN', { month:'long', day:'numeric', weekday:'long' })`(`ComponentNode.vue:134-135`),如 `11月12日星期三`。
- ClassIsland 基准:`ddd MM/dd` 格式(`repos/ClassIsland/ClassIsland/Controls/Components/DateComponent.axaml:11`),即 `周三 11/12`。
- **改造**:默认格式改为 `周三 11/12`(短星期 + 补零月日 + 斜杠),保留 `zh-CN` 区域;字号 `var(--ci-fs-body)`(16px)、`white-space: nowrap`、`font-variant-numeric: tabular-nums`。
- 空数据兜底:`now` 异常时显示 `--`,不得抛错。
- 动画:无。
#### 7.4.2 `clock` 时钟
- 数据字段:`settings.hour12`(默认 `false`)、`settings.seconds`(默认 `true`,`island-layout.ts:182`)。
- 渲染:`toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit', ...(seconds?{second:'2-digit'}:{}), hour12 })`(`ComponentNode.vue:136-142`)。
- ClassIsland 基准:`HH:mm` / `HH:mm:ss`、冒号闪动(`repos/ClassIsland/ClassIsland/Controls/Components/ClockComponent.axaml.cs:71-76`)。
- **改造**:
- 秒数默认改 `false`(与 ClassIsland 一致,`Models/ComponentSettings/ClockComponentSettings.cs` 的 `ShowSeconds=false`),旧配置不动;
- 新增 `settings.flashSeparator`(默认 `false`,P2 才做闪动);闪动规则 `sep = !flash || seconds || 秒%2===1`;
- 字号 `var(--ci-fs-emphasized)`(18px)、`font-variant-numeric: tabular-nums`、`white-space: nowrap`(现有 `:305-308` 已有)。
- 空数据兜底:无。
- 动画:秒变化不做过渡(避免数字滚动抖动)。
#### 7.4.3 `weather` 天气简报
- 数据字段:`settings.locationName / latitude / longitude`(`island-layout.ts:87-90`)。
- 数据源:`bridge.getIslandWeather({ latitude, longitude })`(`preload/island.ts:15`),Open-Meteo,15 分钟刷新,断网保留缓存并标"缓存"(`docs/app/island.md:45`)。
- 渲染:`WeatherBrief.vue`(现有 2.9KB,需按下面改写)。
```html
<span class="weather-brief" :data-stale="stale">
<icon class="weather-icon" /> <!-- 22×22,用 ComponentIcon 或天气码映射 -->
<span class="weather-temp">
<span class="value">23</span><span class="unit">°C</span>
</span>
<span v-if="windSpeed" class="weather-wind">…</span>
</span>
```
- ClassIsland 基准(`repos/ClassIsland/ClassIsland/Controls/Components/WeatherComponent.axaml:51-184`):横排 `gap 6`;图标 22×22;温度值 + 单位 `margin-left:6`;湿度/风/气压/体感/AQI 为可选主信息(一次只显示一种);AQI 6 级配色;预警药丸高 22、圆角 11、文字 14px。
- **改造**(P1 做图标 + 温度;其余 P2):
- 图标尺寸 22×22、`margin-right: 6px`;
- 温度 `font-size: var(--ci-fs-body)`(16px);
- 地点缺失(`latitude/longitude === null`)→ 显示"未设置地点"(`opacity: 0.7`),不得显示 `NaN`;
- 数据缺失 → 只显示图标占位 + `--`;
- `stale === true` → 追加小字"缓存"(`var(--ci-fs-caption)`,`opacity: 0.6`)。
- 动画:数据刷新时数字变化不做过渡(避免每 15 分钟闪一下)。
#### 7.4.4 `countdown` 倒计时
- 数据字段(`island-layout.ts:85-88`):`target: 'auto'|'next'|'end'|'school'|'date'`、`targetDate`、`countdownTitle`。
- 渲染逻辑:`ComponentNode.vue:150-176`(自动/下一节/本节下课/放学/指定日期五种目标,文案 `距上课 m:ss` / `距下课 m:ss` / `距放学 m:ss` / `距X N天`)。
- ClassIsland 基准(`repos/ClassIsland/ClassIsland/Controls/Components/CountDownComponent.axaml:64-114`):`距离 <名称> 还有 <数值>`,紧凑模式去掉"距离/还有";进度环 22×22(或底部进度条);自定义格式 token `%D/%H/%M/%S/%P/%p/%L/%d/%h/%m/%s/%x`。
- **改造(P1)**:
- 文案结构改为 `distance`(默认"距离")+ `countdownTitle` + `connector`(默认"还有")+ 数值;新增 `settings.compact`(默认 `false`);
- 新增 `settings.showProgress`(默认 `false`)+ `settings.progressMode: 'ring'|'bar'`(默认 `ring`):ring 用 22×22 SVG 弧(`stroke-width 3.1`、`stroke-linecap: round`、半径 9.5),bar 用 3px 底部进度条;
- 数值格式化:`%m/%s` 补两位、`%D/%H/%M/%S` 向上取整(与 ClassIsland 一致),默认格式 `%m:%s`(`mm:ss`);
- 颜色:`var(--ci-text-1)`;`settings.color` 覆盖(P2)。
- 空数据兜底:无课程数据 → `今日课程已结束`(现有 `:170`);目标日期为空 → `请设置目标日期`(`:152`)。
- 动画:进度条 `transition: transform 1s linear`;ring 的 `stroke-dashoffset` 用 1s 线性过渡。
#### 7.4.5 `schedule` 课程表
最复杂,必须逐条对照 ClassIsland。
- 数据字段:`settings.display: 'current'|'next'|'today'`(默认 `current`)、`settings.count`(默认 5,范围 1–40)、`settings.showPeriod`(默认 `true`)、`settings.showProgress`(默认 `true`)。
- 数据源:`schedules` + `timePoints`(`ComponentNode.vue:30-61`),时间点优先用服务端完整时间表,缺失时回退课表条目。
- 显示规则(现有,保留):`ComponentNode.vue:74-97`——
- `current`:当前时间点;无则下一节;
- `next`:下一节课 1 条;
- `today`:过滤 `DIVIDER`、当前时间点、未隐藏的 `LESSON`,按 `count` 取窗口并让当前项居中。
- ClassIsland 基准(`repos/ClassIsland/ClassIsland/Controls/Components/ScheduleComponent.axaml` + `LessonControlExpanded.axaml`):
- 每个时间点一个"课程 chip":左右各 `16px × ScheduleSpacing`(默认 1)、科目名 `18px Bold`、附加信息 14px、进度条 3px;
- 隐藏规则:`HideFinishedClass`、`ShowCurrentLessonOnlyOnClass`、`IsHideDefault`、课间只在当前时展开;
- 明天课表 4 种模式(P2);
- 空状态占位"今天没有课程。"/"今日课程已全部结束。"。
- **改造(P1)**:
```html
<div class="node node-schedule" :data-display="display">
<div v-for="p in schedulePoints" :key="p.id" class="lesson-line" :class="{ current, break: p.timeType==='BREAK' }">
<strong class="lesson-name">{{ p.courseName || p.name || '—' }}</strong>
<span v-if="isEnding" class="lesson-ending-badge">-{{ remaining }}</span> <!-- 30×30 正圆,见 lesson-ending.css -->
<small v-if="current && settings.showPeriod" class="lesson-time">{{ p.startTime }}–{{ p.endTime }}</small>
<span v-if="current && p.timeType==='LESSON' && settings.showProgress" class="lesson-progress">
<i :style="{ transform: `scaleX(${elapsed})` }" />
</span>
</div>
<span v-if="!schedulePoints.length" class="lesson-empty">今天没有课程。</span>
</div>
```
- 课程名 `var(--ci-fs-emphasized)`(18px)+ `font-weight: 700`;时间/附加信息 `var(--ci-fs-secondary)`(14px);
- 行内 gap 6px(现状 `ComponentNode.vue:313-321` 已是 6px,保留);
- 进度条 3px(7.2.4);
- 已结束课程 `opacity: 0.6`(ClassIsland `LessonsListBox.axaml:56-58`),过渡 150ms `cubic-bezier(0.645,0.045,0.355,1)`;
- 分割线:`1×18`、`opacity: 0.25`(现状 `:334-342`),可提到 24px 与 ClassIsland 的 25px 对齐(P2);
- 课后占位:现有"今日课程已结束"只用于 countdown;schedule 需新增 `今天没有课程。`/`今日课程已全部结束。` 两态(ClassIsland `LessonControlSettings` 默认文案)。
- 空数据兜底:`timePoints` 与 `schedules` 都为空 → 显示占位、不占高度超过 40px;只有 `schedules` → 回退按条目渲染(现有逻辑)。
- 动画:切换时间点时课程名不做滑动(ClassIsland 是整行裁切),只做进度条与徽标的数值更新。
#### 7.4.6 `text` 文本
- 数据字段:`settings.text`(默认 `'班级信息'`)、`settings.template`(默认 `false`)。
- 渲染:模板变量 `{date}` / `{time}` / `{course}`(`ComponentNode.vue:143-149`)。
- ClassIsland 基准:`TextComponent`(文本内容 + 自定义字体颜色 + 字体大小 16–30)。
- **改造**:字号 `var(--ci-fs-body)`;超长 `text-overflow: ellipsis`(现有 `:125-129` 已有);模板变量保留;新增 `settings.fontSize`(默认 16,范围 16–30)写入 inline `font-size`。
- 空数据兜底:`text` 为空 → `display: none`(不占宽度与间距)。
- 动画:无。
#### 7.4.7 `separator` 分割线
- 数据字段:无。
- 现状:`1px × 24px`、`opacity: 0.25`(`ComponentNode.vue:369-377`,模板 `:249`)。
- ClassIsland 基准:`2×25`、`StrokeThickness=2`、`opacity 0.8`(`repos/ClassIsland/ClassIsland/Controls/Components/SeparatorComponent.axaml:10-14`)。
- **改造**:宽 `2px`、高 `25px`、颜色 `var(--ci-text-fill-secondary)`(`rgba(255,255,255,0.72)`)、`opacity: 0.8`。
- 空数据兜底:无。
- 动画:无。
#### 7.4.8 `slide` 轮播容器
- 数据字段:`settings.interval`(默认 5,范围 1–300)、`settings.direction`、`settings.gap`。
- 现状:非编辑时按 `floor(now / (interval*1000)) % children.length` 只渲染一个子项(`ComponentNode.vue:121-130`),**无过渡动画**。
- ClassIsland 基准:切换间隔默认 15s、三种模式(循环/随机/往复)、启用动画时 250ms 上滑淡入 + 上滑淡出、`cubic-bezier(0.65,0,0.35,1)`、位移 ±40px(`repos/ClassIsland/ClassIsland/Controls/Components/SlideComponent.axaml:72-107`)。
- **改造(P1)**:
- 新增 `settings.interval` 默认改 **15**(旧配置保留)、范围 1–300;
- 新增 `settings.slideMode: 'loop'|'random'|'repeat'`(默认 `loop`);
- 新增 `settings.animate`(默认 `false`):开启后切换用 CSS 类 `slide-in/slide-out`,`250ms`、`translateY(-40px → 0)`、`opacity 0 → 1`;关闭时直接换(现状);
- 子项 `max-height: var(--ci-line-height)`。
- 空数据兜底:0 子项 → 塌缩为 0 宽;1 子项 → 不轮播。
- 动画:`prefers-reduced-motion` 时关闭过渡。
#### 7.4.9 `rolling` 滚动容器
- 数据字段:`settings.speed`(默认 30,范围 1–200)。
- 现状:CSS 动画 `rolling`(`translateX(30%) → -80%`)+ `--rolling-duration: max(1, 200/speed)s`,无限线性(`ComponentNode.vue:384-392, 446-453`,`:205`)。
- ClassIsland 基准(`repos/ClassIsland/ClassIsland/Controls/Components/RollingComponent.axaml.cs:85-135`):仅当内容宽 > 容器宽才滚动;`dur = innerW / speed`、可暂停、可回程;速度默认 40px/s。
- **改造(P1)**:
- 默认速度改 **40**;`--rolling-duration: calc(var(--rolling-distance) / var(--rolling-speed) * 1s)`;
- 仅当 `scrollWidth > clientWidth` 时加 `rolling` 类(用 JS 测量,`ResizeObserver` 监听容器与内容);
- 保留"编辑时不滚动"(`:390-392`);
- 暂停/停止规则 P2。
- 空数据兜底:内容不溢出 → 静止、不抖动。
- 动画:`linear`、无限循环;`prefers-reduced-motion` 时静止。
#### 7.4.10 `group` 分组容器
- 数据字段:`children`、`settings.direction`(默认 `horizontal`)、`settings.gap`(默认 8)。
- 现状:`display:flex`、`gap`、`align-items: center`(`ComponentNode.vue:206-208, 378-380`)。
- ClassIsland 基准:分组容器 = 横向 `StackPanel` + `Margin="-6 0"` + 子项 `Margin="6 0"`(`repos/ClassIsland/ClassIsland/Controls/Components/GroupComponent.axaml:13-33`),即子项间距 12。
- **改造**:`gap` 默认改 **12**;子项外边距 0;容器自身不带内边距。
- 空数据兜底:0 子项 → 0 宽。
- 动画:无(子项变化时行宽由行过渡)。
#### 7.4.11 `stack` 堆叠容器
- 数据字段:`children`、`settings.direction`、`settings.gap`。
- 现状:`flex-wrap: wrap`(`ComponentNode.vue:381-383`)——这是"换行排列",**不是 ClassIsland 的堆叠语义**。
- ClassIsland 基准:`Panel`,所有子项占据同一矩形、各自按对齐摆放(`repos/ClassIsland/ClassIsland/Controls/Components/StackComponent.axaml:12-32`)。
- **改造(P1)**:`.stack { display: grid; }`,所有子项 `grid-area: 1 / 1`,`align-self/justify-self` 由 `advanced.align` 决定;容器宽高取子项测量最大值;子项间距 0。
- 空数据兜底:0 子项 → 0 宽。
- 动画:无。
#### 7.4.12 消息入口 `notification` / `homework` / `call`
这三个 kind 不是普通内容组件,而是"消息入口":
- 现状:渲染时在 `ComponentNode` 里走分支(`ComponentNode.vue:285-288`),只有 `node.id === alertNodeId` 时才渲染 `<slot name="alerts">`,否则编辑态显示文案;数据来自 `IslandState.active/queued`。
- 组件库只展示前 11 类(`docs/app/island.md:40`),消息类由铃铛与消息面板承担。
- **ClassIsland 对应**:通知行/提醒气泡(行内遮罩 + 浮层 + 进度条,`repos/ClassIsland/ClassIsland/XamlThemes/FluentTheme/Styles.axaml:70-202`)。
- **改造(P1)**:
- 保留三个 kind 作为"入口标记":`notification` = 通知入口、`homework` = 作业入口、`call` = 叫人入口;
- 铃铛显示逻辑(`CustomIslandRuntime.vue:421-441`)改为:有消息时显示图标 + 数量徽标(`99+` 上限),无消息时显示 chevron;铃铛图标沿用 `ComponentIcon` 的 `notification` 路径;
- 徽标尺寸保持 14px 高、圆角 7、`#f45b58`(`custom-island.css:94-110`),文字 9px/600;**ClassIsland 风格下**允许改为 `var(--ci-accent-fill)` 底 + 白字(一行样式覆盖);
- 消息卡本体的内容与业务由第 8 部分规定;本部分只规定容器:
- `custom` 详情页:高度 = `islandListLayout().height`(`constants.ts:445+`),宽度 = 岛上可用宽度,位置在 `resident-detail-page`;
- `fixed + classisland`:**固定 40px 行内气泡**(7.2.8),`islandListLayout()` **不参与**;
- `fixed + winisland`:沿用现有详情卡高度 = `islandListLayout().height`。
- 空数据兜底:无消息 → 三个入口都不渲染(`hidden` 计算已处理,`ComponentNode.vue:108-120`)。
- 动画:徽标出现 `resident-indicator-in`(`custom-runtime.css:147-149, 199-206`)保留;`custom` 详情页展开/收起走高度弹簧;`fixed + classisland` 走 7.2.8 的遮罩/文字动画,**不做高度动画**。
#### 7.4.13 `CoursewareTransfer` 课件覆盖
- 触发:`state.coursewareTransfer` 非空时,`LayoutIslandApp.vue:58, 71-75` 直接渲染课件卡,并让经典岛 `suspended`、自定义岛不渲染(`:57`)。
- 现状视觉:`position: fixed`、尺寸 `COURSEWARE_ISLAND_SIZE`、`padding 17px 24px`、`border-radius 24px`、底色 `rgba(20,20,24,0.97)`、阴影 `0 2px 8px #0004`、字号 13/1.4、进度条 5px(`CoursewareTransfer.vue:67-131`)。
- ClassIsland 对应:课件传输不在 ClassIsland 中,属 ClassHelper 独有能力 → **保留现状视觉**,只做两点对齐:
1. 锚点/边距与岛一致(左/中/右 + `ISLAND_SHADOW_PAD`,`CoursewareTransfer.vue:12-25`),尺寸不随 `style` 变化;
2. `pointer-events: none` 保留(`:81`),并由 `setHitRect` 上报矩形(`:35-38`),保证不吞桌面点击。
- 空数据兜底:`transfer.error` 存在时显示错误行(`:64`),不隐藏整卡。
#### 7.4.14 组件默认值总表(必须与代码一致)
`islandNode()` 当前默认(`island-layout.ts:173-216`)必须逐项落实;本轮改动用 **粗体** 标注:
| 字段 | 当前默认 | 本轮目标 |
|---|---|---|
| `settings.text` | `班级信息` | 保留 |
| `settings.template` | `false` | 保留 |
| `settings.hour12` | `false` | 保留 |
| `settings.seconds` | `true` | 保留(新节点默认仍 true,兼容) |
| `settings.target` | `auto` | 保留 |
| `settings.targetDate` | `''` | 保留 |
| `settings.countdownTitle` | `目标日` | 保留 |
| `settings.locationName` | `''` | 保留 |
| `settings.latitude/longitude` | `null` | 保留 |
| `settings.display` | `current` | 保留 |
| `settings.count` | `5` | 保留 |
| `settings.showPeriod` | `true` | 保留 |
| `settings.showProgress` | `true` | 保留 |
| `settings.direction` | `horizontal` | 保留 |
| `settings.gap` | `8` | **12**(组/堆叠容器子项间距) |
| `settings.interval` | `5` | **15**(轮播间隔,秒) |
| `settings.speed` | `30` | **40**(滚动 px/s) |
| `appearance.*` 字号 | 14/16/18/20 | 保留(改用 `--ci-fs-*`) |
| `appearance.background` | `#202024` | 保留 |
| `appearance.backgroundOpacity` | `0.5` | 保留 |
| `appearance.cornerRadius` | `16` | **8**(与 ClassIsland 圆角一致) |
| `appearance.opacity` | `1` | 保留 |
| `advanced.align` | `stretch` | 保留 |
| `advanced.width/maxWidth` | `0/0` | 保留 |
### 7.5 编辑器渲染侧结构(组件库 / 拖拽 / 选中框 / 行设置)
编辑器是主窗口里的设置页(`IslandEditorView.vue`,860 行),**不是**岛的渲染进程;但它的 DOM/CSS 必须与岛保持同一套 token,避免"编辑器里好看、上岛变形"。
#### 7.5.1 两态:标签排列 vs 实际预览
- 非编辑态(`editMode=false`):`component-lines` 排列行与组件 chip(`IslandEditorView.vue:659-695`),每行一个 `.component-line`。
- 编辑态(`editMode=true`):`island-preview` 按方案的真实布局渲染(`:698-738`),支持预览缩放 `0.3–1.5`、步进 `0.1`、重置(`:739-750`)。
- **必须保留**:"编辑预览不创建额外桌面窗口"(`docs/app/island.md:47`)。
#### 7.5.2 尺寸与选中框(ClassIsland 规格)
| 元素 | 现状 | 目标 |
|---|---|---|
| 行容器 `.component-line` | `min-height: 54px`、`padding: 4px`、`border: 1px solid transparent`、`radius: var(--ch-radius-control)`(`island-editor.css:134-141`) | `min-height: 48px`(行高 40 + 上下 4)、`padding: 4px 8px` |
| 行把手 `.row-handle` | `width: 24px`(`:149-154`) | 保留 24px |
| 组件 selecetd | `background: var(--ch-accent-soft)`、`border-color: var(--ch-accent)`(`IslandEditorNode.vue:86-89`) | **改为 1.5px 虚线**:`outline: 1.5px dashed var(--ci-accent)`;`outline-offset: -1.5px`;不加背景色 |
| 容器组件 | `border: 1px dashed var(--ch-border-strong)`、`padding: 3px`(`:90-94`) | `1.5px dashed`、`padding: 3px`、`border-radius: 8px` |
| 拖入指示 | `.drop-before/after` 3px 强调色竖条(`:111-123`);`.drop-inside` 2px 实线(`:107-110`) | 保留,颜色改 `var(--ci-accent)`、圆角 1.5px |
| 拖拽幽灵 | `.drag-ghost`(`IslandEditorView.vue:774-780`) | 保留,`padding: 4px 8px`、`border-radius: 6px`、背景 `var(--ci-bg-tertiary)` |
| 详情抽屉 | `el-drawer` 宽 `min(360px, 94vw)`(`:785`) | 保留 |
#### 7.5.3 行设置面板字段
现有行面板(`IslandEditorView.vue:804-831`):当前行、行名称(≤64 字)、组件间距(0–64)、允许显示提醒、设为主要行、上移/下移/删除。
**目标态新增(与 ClassIsland 对齐,P1)**:
- 行不透明度 `opacity`(默认 1.0,范围 0.1–1,步进 0.05)——写在行 `style.opacity`;
- 行隐藏规则 `hideOnRule`(默认 false)+ 规则集(P2);
- 行圆角覆盖(P2)、行背景色/不透明度覆盖(P2,对应 ClassIsland 行级"自定义背景颜色/不透明度/圆角")。
保存路径不变:`bridge.saveLayout()`(`preload/island.ts:17`)→ `island:save-layout`。
#### 7.5.4 组件库
- 11 个条目(`date/clock/weather/countdown/schedule/text/separator/slide/rolling/group/stack`),每项 `ComponentIcon` + 名称 + 描述(`IslandEditorView.vue:751-770`,数据来自 `ISLAND_COMPONENT_LABELS` + 本地描述表)。
- 交互:`pointerdown` 开始拖拽、`click` 点击添加(`:760-761`)。
- 目标态:图标尺寸 18×18(`IslandEditorNode.vue:74-78`)保持不变;卡片 `min-height: 76px`(`island-editor.css:285`)保留;**不得**把 `notification/homework/call` 加回组件库(`docs/app/island.md:40` 的既有约定)。
#### 7.5.5 渲染侧必须新增/调整的 DOM 与 token
必须新增(岛渲染进程):
- `.ci-row-shell`(`classisland` 风格 fixed 模式的行容器);
- `.ci-mask-layer` / `.ci-mask-title`(7.2.6);
- `.lesson-empty`(课程表占位);
- `.resident-bell` 的 `[data-family='classisland']` 覆盖;
- `.layout-row[data-hide='true']`(隐藏规则命中)。
必须新增(CSS 变量,挂在 `:root` 与 `.resident-card` 上):
```css
--ci-line-height: 40px; --ci-line-pad-x: 12px; --ci-line-gap: 5px; --ci-component-gap: 12px;
--ci-radius-island: 8px; --ci-island-opacity: 0.5; --ci-line-opacity: 1;
--ci-stroke-island: #ffffff26; --ci-stroke-island-w: 1px;
--ci-shadow-island: 0 4px 8px 2px #48000000;
--ci-progress-height: 3px;
--ci-fade-ratio: 0.05;
--ci-fs-secondary: 14px; --ci-fs-body: 16px; --ci-fs-emphasized: 18px; --ci-fs-large: 20px;
--ci-bg-secondary: #20202080; --ci-text-1: rgba(255,255,255,0.92); --ci-text-2: rgba(255,255,255,0.72);
--ci-accent: (用户强调色); --ci-accent-fill: color-mix(in srgb, var(--ci-accent) 100%, transparent);
```
**命名对齐**:`packages/shared/src/island-layout.ts:20-36` 的 `CLASSISLAND_ISLAND_METRICS` 已经是这些值的单一来源(`rowHeight:40`、`horizontalPadding:12`、`radius:8`、`opacity:0.5`、`rowSpacing:5`、字号 14/16/18/20、`fadedOpacity:0.05`)。渲染层**必须**从该常量生成 CSS 变量(通过 `style` 内联或 `applyAppearance` 写入 `document.documentElement.style`,位置同 `CustomIslandRuntime.vue:102-124`),**不得**在 CSS 里再抄一份数字。
### 7.6 验收标准
实现者必须逐条自查,并**只在显式 `--visual` 的视觉验证里**做桌面像素比对(默认后台验证不显示岛):
1. 默认值:新安装 `appearance.family = 'classisland'`;旧配置(无 `family`)升级后 `family = 'winisland'` 且 fixed 观感与升级前一致,不报错、不丢组件数据;设置页可一键切换并即时预览、可回退。
1.1 **`family` 只影响 fixed**:`layout='custom'` 时切 `family` 不改变任何渲染(custom 恒 ClassIsland 规格),设置页在 custom 下不展示"外观族/视觉风格"两项;`custom` 无 winisland 回退档。
2. `family='classisland'` 的 **fixed 模式是信息行不是胶囊**:默认 1 行 = 日期 + 课程表、行高 40、圆角 8、背景不透明度 0.5、底部 3px 进度条;无消息时不显示(或按 `idleSliver` 留细缝);消息到来时在**同一条 40px 行内**渲染 ClassIsland 气泡(斜切遮罩 + 标题 + 正文 + 倒计时进度),收起后回到常态行。
2.1 **气泡不增高行**:气泡出现/消失前后,行高恒 40px(像素量测误差 ≤1px),窗口高恒 40+2×pad,`win.getBounds()` 不变;正文最多 2 行、超出省略号,不得出现文字溢出到行外或行高变成 48/56/64。
2.2 气泡动画参数与 ClassIsland 一致:遮罩每段 360ms、错峰 0/90/90/120/120ms(总 480ms)、开 QuarticEaseOut / 关 QuadraticEaseIn;文字 250ms delay 260ms + scale 1.1→1.0/750ms;退出 200ms、缩回 `cubic-bezier(0.5,0,0.75,0)`;`theme='classic'` 走 translateY 变体。
3. `family='winisland'` 的 fixed 模式仍是胶囊 + 详情卡:胶囊 268×44、圆角 `h/2`、squircle、纯黑/玻璃/渐变三色调、展开态阴影 `0 2px 3px rgba(0,0,0,.11)`;与改造前逐像素一致。
4. 岛在 6 个停靠位置、两个显示器、125%/150% DPI 下位置正确:居中停靠不受 `marginX` 影响;左右停靠距屏幕边缘 = `marginX`;底停靠时窗口贴底且岛不被裁。
5. 窗口几何:开合过程中 `win.getBounds()` **不变化**(用主进程日志或注入断言验证);只有设置变化/不可见时才 `setBounds`;`classisland` fixed 的窗口高恒为 40 + 2×`ISLAND_SHADOW_PAD`。
6. 行:可见行高 `40px`(像素量测)、行内边距 12、行距 5、圆角 8、描边 1、阴影 `0 4 8 2 #48000000`;相邻组件间距 12;进度条 3px。
7. 背景不透明度 0.5 只影响背景:文字在截图里为纯白 0.92/0.72 层级,不随背景变透明。
8. 三条进度条高度都是 3px(课程、课程提醒、通知气泡),且不出现 2px 残留。
9. `custom` 模式:`rows` 与 `expandedRows` 都按行渲染;`scheme.separated=true` 时每行独立背景胶囊,无整块背景;**custom 渲染恒按 ClassIsland 规格,切换 `family` 不产生任何视觉变化**。
9.1 **custom 不被课程提醒接管**:prepare / on-class / break / in-class 期间岛仍显示常态行与铃铛(课程进度由 `schedule` 组件呈现),不出现整岛替换;`ending` 只在课程名右侧显示蓝色圆形秒数徽标(30×30、`-N`);不存在"临时接管一行"的行为。
10. 遮罩:通知/课程提醒共用 `SlantedMask`,5 段从两侧向中间打开(总 480ms)、关闭反向;开启减少动态效果或关闭动画时直接落位。
11. 触摸屏:手指能点开岛(触摸模式生效),窗口贴合岛体,失焦宽限 1200ms;非触摸屏行为与改造前完全一致。
12. 鼠标穿透:岛外点击一律落到桌面;命中矩形随形变更新(限流 50ms),形变结束后无"看不见却点不动"的死区。
13. 11 个组件逐项:`date` 显示 `周三 11/12`、`clock` 显示 `HH:mm`、`weather` 显示 22px 图标 + 温度且无数据时不出现 `NaN`、`countdown` 文案与进度环/条可选、`schedule` 当前项高亮 + 3px 进度 + 已结束 0.6 淡化 + 空占位、`text` 空内容不占宽、`separator` 2×25/0.8、`slide` 15s 切换且可关动画、`rolling` 仅溢出时滚动且 40px/s、`group` 间距 12、`stack` 子项完全重叠。
14. 消息入口:铃铛数量徽标正确(1–99,>99 显示 `99+`),无消息显示 chevron;`notification/homework/call` 不在组件库出现,但旧方案里的节点仍能渲染。
15. 课件覆盖:传输中出现课件卡且岛隐藏;锚点与边距正确;`pointer-events: none`,不吞点击。
16. 编辑器:选中组件为 1.5px 虚线强调框;容器组件为 1.5px 虚线;拖入指示在 before/after/inside 三种位置正确;预览缩放 0.3–1.5 不导致岛变形错位。
17. `prefers-reduced-motion` 与设置里的"动画"开关都生效:关闭时所有弹簧 `snap()`、滚动静止、遮罩直接落位。
18. 不得出现:`setBackgroundMaterial`、`backdrop-filter`、第二个 `BrowserWindow`、把窗口尺寸写进 CSS 的做法、在动画帧里 `setBounds`。