## 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
```
强制要求:
- 行容器类名保持 `.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
```
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
…常态行组件…
…
紧急
…单行省略…
…最多 2 行…
…按钮…
```
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
```
- `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
23°C
…
```
- 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
{{ p.courseName || p.name || '—' }}
-{{ remaining }}
{{ p.startTime }}–{{ p.endTime }}
今天没有课程。
```
- 课程名 `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` 时才渲染 ``,否则编辑态显示文案;数据来自 `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`。