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

112 KiB
Raw Blame History

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 四档派生公式(必须逐字实现):

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 或直接落位

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):

# 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,需按此重排):

<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 为主):

.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 */
}

行本体:

.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 变量:

.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)。

背景(两种形态都要):

/* 整块岛背景(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 外边距"。

改造:

.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 对齐:
<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>
  1. 遮罩填充色 = var(--ci-accent-fill)(ClassIsland 用 AccentFillColorDefaultBrush);文字色 = var(--ci-text-on-accent)。
  2. prefers-reduced-motion 或 appearance.animations === false 时直接落位(regions 全部 0/1),不做逐帧插值(LessonReminderContent.vue:77-80 已有该分支,保留)。
  3. 不得给遮罩层加 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):

<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 规格:

.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)。改造要求:

<!-- 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,需按下面改写)。
<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):
<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 上):

--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。