diff --git a/.changeset/better-trees-begin.md b/.changeset/better-trees-begin.md
new file mode 100644
index 0000000..a11b0b2
--- /dev/null
+++ b/.changeset/better-trees-begin.md
@@ -0,0 +1,9 @@
+---
+'@inkcre/ui-web': patch
+---
+
+将 DESIGN.md 调整为共同设计总纲与按任务阅读的入口,分别维护设计立场、视觉语言、页面组合和判断依据,使设计选择与理由便于按任务查阅。
+
+明确局部等宽混排、中性灰与少量彩色重点、默认直角、平面层次和空间节奏,并补充内容取舍、按状态与层级呈现及中西文排印参考。
+
+设计正文和带有适用范围说明的实例图片随包交付,消费与维护入口同步更新;生成和安装包检查覆盖整组文件、内容及相对链接。
diff --git a/.changeset/clear-header-menu.md b/.changeset/clear-header-menu.md
new file mode 100644
index 0000000..7c95f96
--- /dev/null
+++ b/.changeset/clear-header-menu.md
@@ -0,0 +1,5 @@
+---
+"@inkcre/ui-web": patch
+---
+
+修复 Header 默认菜单图标被按钮透明背景覆盖而不可见的问题。按钮与装饰图标分别负责交互和绘制,保留菜单名称、键盘操作及 menu-click 事件。
diff --git a/.changeset/goofy-bears-brake.md b/.changeset/goofy-bears-brake.md
new file mode 100644
index 0000000..9a3c3e5
--- /dev/null
+++ b/.changeset/goofy-bears-brake.md
@@ -0,0 +1,7 @@
+---
+"@inkcre/ui-web": patch
+---
+
+浅深主题的主动作与 Switch 采用中性灰配对,深色基底与相邻表面缩小明暗跳变;危险按钮使用灰红底色,需要注意的反馈文字采用较纯的绿、金黄、蓝和红。同步普通、悬停、按下、等待及反色文字的配对,并缩小覆盖层阴影的偏移与模糊范围。
+
+保留公开角色与调用方式,品牌基础色映射到中性色阶;危险动作底色与错误前景分别维护。按用途区分语义文字与动作底色,修正默认成功着色的指南、Agent 配方和表单示例。
diff --git a/.changeset/neat-chicken-type.md b/.changeset/neat-chicken-type.md
new file mode 100644
index 0000000..11d8a6d
--- /dev/null
+++ b/.changeset/neat-chicken-type.md
@@ -0,0 +1,7 @@
+---
+'@inkcre/ui-web': patch
+---
+
+Switch 普通状态文案采用系统 UI 字体,继续由两种文案共同维持切换与等待时的轨道宽度。AutoForm 根级错误容器采用默认直角轮廓。
+
+同步 AutoForm 和 Image 示例的字体、主题配色与轮廓,图片示例使用准确的替代文字、持续可见的查看提示和可实际下载的原图链接。
diff --git a/.gitignore b/.gitignore
index 292395f..c8df355 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,10 @@ coverage/
*.png
*.jpg
+# Published design examples are maintained documentation assets.
+!/docs/design/examples/*
+!/packages/web/docs/design/examples/*
+
# svc:begin local-config sha256=0cb2591848c4e5675766aa516f16dfb9981fc52e1dcbadd8cec89b6982db927d
svc.local.json
AGENTS.local.md
diff --git a/AGENTS.md b/AGENTS.md
index b1ae329..0b5dc97 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,7 @@ Reason in English. Communicate with humans in Chinese.
## Knowledge Owners
-UI 设计、组件、Token 或视觉审视工作先读取 [DESIGN.md](DESIGN.md),再按任务读取具体实现与指南。纯构建和非 UI 工作不需要加载全部设计上下文。DESIGN.md 拥有共同设计判断,具体值与 API 仍由下面的源头负责。
+UI 设计、组件、Token 或视觉审视工作从 [DESIGN.md](DESIGN.md) 的总纲和阅读路径进入,只读取与任务有关的设计正文。`docs/design/` 分别维护设计立场、视觉语言、页面组合与判断依据;纯构建和非 UI 工作无需加载这些正文。具体值与 API 仍由下面的源头负责。
- Repository and package entry points: `README.md` and `packages/web/README.md`.
- Public package API: package exports, component manifest, TypeScript source,
@@ -29,6 +29,14 @@ UI 设计、组件、Token 或视觉审视工作先读取 [DESIGN.md](DESIGN.md)
scripts, and `.github/workflows/`.
- Repeated subtree hazards: the nearest local `AGENTS.md`.
+## 设计变更的维护责任
+
+先明确变化要保持的设计关系,再确定修改位置。已有角色是否可复用,取决于共同用途与共同变化理由;新增共享能力应说明实际情境、现有缺口、默认行为、允许变化和覆盖责任。可以在变更说明中自然表达,无须另建统一模板。
+
+设计正文拥有已确认的选择及其理由,Token 源拥有名称、类型、值与引用,组件源码和声明拥有 API 事实。实现观察、提炼过程、待确认方案与讨论状态记录在 task packet,确认后的结论才进入正式正文。当前表现与认可要求不一致时应明确指出差异,按任务授权修复或提出规则变更,不能只为解释代码而修改要求。
+
+改变公开名称、用途、默认表现或交互承诺时,同步相应实现、设计正文、示例和迁移说明。静态检查确认交付事实,真实场景确认设计关系与表现;视觉验收需要说明比较依据,不能只报告 Token 合法或构建成功。
+
## Coding Guidelines
- [Coding for Human](.github/instructions/coding-for-human.instructions.md)
diff --git a/DESIGN.md b/DESIGN.md
index 5c2d3dc..08d9ab6 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,77 +1,16 @@
-# InKCre 设计决策指南
+# InKCre 设计
-这份指南面向使用和维护 InKCre 设计系统的人与 Agent。它说明界面应保持的关系、默认选择,以及变化应该由谁负责。消费者用它选择和组合能力,维护者用它判断现有实现和新增能力是否合适。具体 API、安装与调用从所在仓库或安装包的 [README](README.md) 继续查阅。
+InKCre 的界面应让内容的关系和可执行的操作容易理解,让阅读、判断与修改有连续的秩序。文字、颜色、边界和空间共同表达这种秩序;具体表现随内容与容器调整,同时保留各自承担的含义。
-仓库根级文件是人工维护的源,Web 包内文件是随版本交付的副本。消费时读取安装版本的指南;维护时读取当前 checkout 的指南。指南中的 Web 能力不表示 Flutter 或 uniapp 已有对应实现。
+这里是人、消费者 Agent 和维护 Agent 的共同入口。设计知识按责任分别维护,阅读与当前任务有关的部分即可。组件 API、Web 调用和工程流程从 [README](README.md) 进入,不在这里重复。
-## 从任务决定界面
+| 当前要作出的判断 | 阅读入口 |
+| -------------------------------------------------- | ------------------------------------------ |
+| 理解共同立场,或判断一项新需求值得保持什么 | [设计立场](docs/design/principles.md) |
+| 选择文字、颜色、形状和层次,或审视整体视觉是否协调 | [视觉语言](docs/design/visual-language.md) |
+| 把内容和操作组织成页面、列表、表单或浮层 | [页面组合](docs/design/composition.md) |
+| 比较方案、解释取舍,或用已有例子校准判断 | [判断依据](docs/design/reference.md) |
-先明确用户要理解什么、改变什么,以及操作何时算完成。按这些关系组织信息、选择组件,再调整视觉。系统提供文字、颜色、空间和交互的共同约束;具体业务导航、权限、数据模型与流程由宿主产品决定。
+设计立场说明理由,视觉语言说明表达,页面组合说明关系如何落到场景,判断依据说明能从实例得出什么结论。它们共享设计方向,各自只拥有相应的知识;一个技术问题不要求加载整套正文。
-默认使用现有组件和语义角色。需要变化时,说明实际内容、容器或状态有什么不同,以及变化解决了什么问题。视觉边界与内容可读、功能可用冲突时,先保护内容与功能,再调整布局和策略。已有页面可以作为证据,但其中的偶然值或缺陷不能自动成为后续标准。
-
-## 文字与视觉层级
-
-按文字的职责选角色。正文需要支持连续阅读,标签需要标识控件或状态,说明需要表达补充信息或错误,标题需要帮助理解页面和区块结构。默认选择如下;名称对应当前系统角色,精确度量由 Token 源维护。
-
-| 内容 | 默认角色 |
-| -------------------------------------- | -------------------------------- |
-| 普通正文/说明、错误、提示 | body-md/body-sm |
-| 控件值与字段标签/短元信息、小按钮状态 | label-lg/label-md |
-| 页面标题/区块与弹层标题 | title-lg/title-sm |
-| 强调正文或引言/醒目页面标题 | body-lg/headline-lg |
-| 代码、标识符 | 合适的文字角色,加 mono 字体选择 |
-
-label-lg 和 body-sm 当前度量相同,但承担不同职责。消费者按用途选择;维护者只有在用途和未来变化责任一致时才合并角色。不能因为说明放不下,就改成更小的标签角色。
-
-默认使用系统 UI 字体,代码可使用等宽字体。家族和装饰与尺寸角色分别选择。Web 不下载字体、不重设 html 字号;相对字体尺度与比例行高表达既定依赖,不能单凭使用了 rem 就宣称所有内容都能适应。
-
-## 配色表达用途和状态
-
-颜色按文字、表面和状态的关系选择,并在实际背景上检查可读性。普通表面的文字选 text.base 或 text.subtle;primary 与 danger 表面分别配 text.on-primary、text.on-danger。反色文字的适用性取决于配对表面,不能因它叫 primary 就用于普通正文。
-
-error、success、warning、info 是普通表面上的反馈前景用途,配合明确的文字表达状态。系统目前没有约定带色反馈容器,不能从前景角色自行推导一套容器角色。必要控件边界用 border.base,装饰分隔用 border.subtle,焦点轮廓用 border.strong。
-
-hover/pressed 表达可执行动作的交互反馈;focus 表达当前键盘位置;error 表达需要处理的问题。它们的责任可以同时存在,例如错误边框不能抹掉焦点提示。pending 保留动作文字与可读配色,显示进行状态并防止重复执行;普通 disabled 使用对应的弱化表现。
-
-覆盖主题颜色时,应连同前景、背景和相关状态一起核对。某个色值自身或一种派生公式不能证明所有配对都可读。图片文字需要稳定的承载表面,不能依赖图片恰好够暗。
-
-## 空间、内容和适应
-
-先用布局、间距和对齐表达分组。字段内部、字段之间、组件内部与页面留白有不同责任,即使此刻数值相同,也不意味着必须一起变化。
-
-长内容优先换行并扩展承载区域;宿主负责页面列数、容器和外部留白,组件负责内部文字与控件的布局。带框输入的默认最小高度允许随文字增长,不应被固定高度重新裁切。无法断开的标识符、长中英文与窄容器应作为相关变化的具体核对场景。
-
-固定值、离散尺度和局部 CSS 都可以表达合理决定。按钮内与文字相关的留白可以跟随文字尺度,页面空间可以依据容器调整;不要求所有间距遵循统一缩放曲线。引入响应规则时,应说清依据哪个上下文、允许怎样变化、边界是什么,以及相对现有方案改善了什么。
-
-例如,页面在宽容器中需要更大的留白,可以由页面布局表达。只有多个场景需要共享同一种空间政策时,才考虑公共角色;一张设计稿中的数值差异不足以支持新增全局密度参数。
-
-## 组件组合与用户状态
-
-普通表单从 InkForm 的纵向布局和内置控件的 label/error 开始。Input、Textarea、Dropdown、Picker 已承担标签关联;自定义字段再用 InkField,避免重复标签。页面负责字段分组、提交和持久化;字段组件负责输入与反馈表达。
-
-按后果区分普通动作、主要动作与危险动作。主提交明确使用相应按钮主题和原生提交类型。异步操作保留动作含义和用户输入,失败应有明确反馈和可恢复路径;不能把异常显示成成功,也不能让校验默认值覆盖已有草稿。
-
-确认场景按所需语义选择:InkDoubleCheck 提供确认弹层,InkDialog 支持更完整的确认、取消与等待过程,自定义弹层才使用 InkPopup。受控模型、确认提交和取消草稿是不同责任,消费时遵循各组件参考,不从外观推断模型提交时机。
-
-加载、空结果、操作失败与确认分别表达不同状态。根据真实可达状态提供界面,不把它们合并为一个含糊的提示,也不为应用不可能进入的状态添加配置。
-
-原始编辑数据与解析结果也要区分。InkJsonEditor 编辑包含不完整 JSON 在内的原始文字;消费者只在验证通过的边界解析和持久化。适合固定业务流程的普通表单和由 schema 驱动的表单,按数据责任选择,不能仅因为后者能自动生成控件就替换前者。
-
-## 主题与实现责任
-
-用户明确选择的主题优先于系统偏好。Web 以根级主题和公开 CSS 变量提供已约定的覆盖能力;哪些值在运行时传播、哪些需要重新构建,由样式指南说明。任意 ref 值的变化不保证自动带动所有 sys 或 comp 输出。
-
-Web 的 Popup/Scrim 会移动到 body,局部容器的主题或字体不能被当成弹层的继承保证。宿主需要统一主题时,应使用已支持的根级入口。跨平台可以共享颜色用途、文字职责和状态要求;CSS、DOM、原生控件与平台交互方式由对应实现负责。
-
-## 维护和扩展
-
-修改前先确定要保持的设计关系与预期变化,再选择改动位置。复用已有角色的依据是共同用途和共同变化责任;局部计算只有形成有意支持的公共能力后才需要成为 Token 或 API。
-
-新增共享能力应说明真实使用情境、现有能力的缺口、默认行为、允许变化和覆盖责任。设计决定、输入格式、求值机制和某次渲染结果分别判断;不要把它们都当成“改一个数字”。这些说明可以放在适当的自然段和例子中,无须为每次调整填写统一模板。
-
-Token 源拥有名称、叶节点用途、类型、值与引用;组件源码和公开声明拥有 API 事实;本指南拥有跨角色与场景的设计判断。发现实现与已认可要求不一致时,应指出差异,在当前授权范围内修复或提出规则变更。不能仅为解释现有代码而修改要求。
-
-变更验证应与其影响对应:静态检查确认类型、生成与包契约,真实页面确认内容、状态、视觉和交互。生成成功不能代替设计判断。只有稳定重复、能够可靠判定的问题才值得加入自动检查;新的品牌主张或尚无证据的通用规则先作为提案讨论。
-
-对公开名称、用途、默认表现或交互承诺的改变,连同实现、指南、示例及迁移说明一起交付。Agent 的操作授权、命令和提交规则由所在仓库的 AGENTS 负责。
+仓库根级 DESIGN.md 与 docs/design 是人工维护的源,Web 包内相同路径是随版本交付的副本。消费者读取安装版本,维护者读取当前 checkout;设计意图可以跨平台延续,具体宿主业务与平台实现由各自负责。
diff --git a/README.md b/README.md
index 700bbc8..b509cbe 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ renderer is published as `@inkcre/ui-web`.
## Single Source of Truth
-设计判断由 [DESIGN.md](DESIGN.md) 维护。`tokens/inkcre.tokens.json` 是 Token 规范源,
+[DESIGN.md](DESIGN.md) 提供设计总纲和按任务阅读的路径;设计立场、视觉语言、页面组合和判断依据由 `docs/design/` 分别维护。`tokens/inkcre.tokens.json` 是 Token 规范源,
Figma 仅提议已有路径的值更新;生成物通过生成器更新。
当前输入格式与维护入口见 [Token 指南](tokens/tokens.md),生成命令与输出见
[Token 生成说明](scripts/build-tokens.md)。
@@ -87,6 +87,8 @@ coverage, and generated Agent Skills.
## Histoire delivery
+展示构建使用 lockfile 中的 Vue 和 Histoire 高亮实现,由 Vite 打包到站点。不要把 Vue 单独替换成 CDN 生产版本,也不要用空对象模拟 Shiki;Histoire 的状态同步和源码面板依赖这些库的实际运行时契约。
+
`UI checks` validates Histoire as part of the repository contract. After a
successful same-repository run, the trusted Preview workflow checks out that
exact pull-request head, builds Histoire itself, and publishes it to the stable
diff --git a/docs/design/composition.md b/docs/design/composition.md
new file mode 100644
index 0000000..e6af3cb
--- /dev/null
+++ b/docs/design/composition.md
@@ -0,0 +1,51 @@
+# 页面组合
+
+本页说明如何让内容、操作和状态在页面中形成关系。[设计立场](principles.md) 说明理由,[视觉语言](visual-language.md) 说明表达;组件的参数和模型时机从 [README](../../README.md) 进入对应参考。
+
+## 从完整任务安排阅读顺序
+
+先让用户辨认正在面对的对象,再安排理解它所需的信息、当前状态和下一步动作。标题、摘要、元数据与操作之间的主次应清楚;元数据接近其解释的内容,局部操作接近其作用对象,页面级操作则有稳定的位置。
+
+页面可以同时容纳阅读和管理,但不必让每一项信息争夺同样的注意。普通列表、内容正文和设置表单的节奏可以不同,宿主负责根据真实任务决定导航与业务流程。
+
+## 决定当前呈现什么
+
+整理内容先于调整间距。重复的标题、复述同一状态的标签、没有帮助的说明和纯粹填空的图标应直接移除;需要保留的内容,再按当前任务决定出现的位置与时机。已有空间不要求被填满。
+
+列表帮助识别和比较对象,详情承载进一步阅读,编辑态提供修改所需的信息。不要把三个层级的全部内容同时摆在列表里。有用但暂时次要的信息可放到展开区、详情或次级操作入口中,入口应可发现,并让人知道能在那里找到什么。若用户必须反复展开才能完成主要任务,就需要重新考虑哪些内容应直接出现。
+
+界面随状态改变当前呈现的内容:保存期间保留输入与进度,失败后及时显示对应问题和恢复动作,无须同时陈列所有可能状态。影响当前判断的警告、校验错误和操作后果直接呈现;不能为了视觉简洁藏到容易错过的入口。
+
+状态类别不自动决定它必须着色。常态的“已连接”、例行保存结果与普通进度说明默认使用中性色;当结果需要特别确认、风险需要处理或变化会影响下一步时,再用语义色强调关键文字或标记,详细说明保持普通文字的阅读层级。多个问题同时存在时,按影响范围与处理优先级组织,不为减少彩色而隐藏必要反馈。
+
+容器变窄时,重新安排次要内容与操作的入口,保留主要任务及必要信息的可达性。切换尺寸、进入详情和返回列表时,草稿、选择与当前上下文应能延续。用完整操作过程检查这些变化,避免只让默认状态或一个尺寸显得简洁。
+
+## 分组与独立容器
+
+连续的说明、属于同一对象的字段、围绕同一结果的元数据,先通过顺序、对齐和间距形成一组。只有当封闭边界进一步说明了独立对象、交互范围或层次时,才增加容器。
+
+例如,一个设置区块可以由标题、说明和纵向字段直接组成。给每个字段再包一层卡片,容易打断整组阅读,并让辅助说明看起来像另一个对象。相反,多个独立来源各有名称、状态和操作时,重复的条目或容器可以帮助辨认对象归属。应该比较这些关系,而不是统一要求所有内容使用或不用卡片。
+
+列表条目具有共同结构时,通过稳定的对齐与顺序帮助比较;需要连续阅读的正文应保留完整内容。摘要是进入全文前的选择帮助,不能继续截断已打开的完整内容。
+
+## 表单围绕修改与提交组织
+
+普通表单从纵向字段开始。标签靠近输入,说明与错误放在相关字段旁边;字段分组表达业务含义,不按底层组件类型分类。自带 label/error 的控件已经承担关联,自定义字段再补对应结构。
+
+主要提交应能被辨认为这次修改的完成动作,取消和危险操作与其保持可理解的区别。校验失败应说明需要修改什么;保存失败应保留已经输入的内容,让重试有连续的上下文。保存期间保留动作文字与可读配色,并防止重复执行。
+
+固定业务流程与 schema 生成表单承担不同的数据责任。先决定用户如何理解和修改内容,再选择实现;具体组合从 Web 的 InkForm、内置字段、InkField 以及现有配方进入。
+
+## 浮层延续当前任务
+
+确认需要让用户看清对象、后果和选择;内容浮层需要让人理解正在阅读什么、如何继续或返回。浮层内部不应因为处于弹层中就再重复一套页面外壳。
+
+普通确认、需要等待的确认和自定义内容浮层各有适用语义。Web 的具体选择由 InkDoubleCheck、InkDialog、InkPopup 的参考说明。背景是否继续可操作、何时提交和何时放弃草稿,都应与当前任务一致,不能从外观推断。
+
+加载、空结果、失败与确认有不同含义,应分别表达。错误反馈不要抹掉键盘焦点,进行状态也不应伪装成普通不可用状态。
+
+## 容器变化时保留关系
+
+宿主决定页面列数、外部留白和阅读区域,组件负责内部内容与操作布局。确定当前需要呈现的内容后,可用空间缩小时让相关内容换行或按既有阅读顺序排列;不要通过缩小正文、遮掉必要操作或裁切说明来维持双栏外观。
+
+同一界面在宽屏与窄容器中可以看起来不同,但对象归属、信息主次和动作含义应能被追踪。标识符可以换行,结构化代码可以局部滚动;局部滚动不应吞掉整个页面的操作入口。具体对照见[判断依据](reference.md)。
diff --git a/docs/design/examples/action-states.png b/docs/design/examples/action-states.png
new file mode 100644
index 0000000..6a11bff
Binary files /dev/null and b/docs/design/examples/action-states.png differ
diff --git a/docs/design/examples/form-content.png b/docs/design/examples/form-content.png
new file mode 100644
index 0000000..6807bdc
Binary files /dev/null and b/docs/design/examples/form-content.png differ
diff --git a/docs/design/principles.md b/docs/design/principles.md
new file mode 100644
index 0000000..644b197
--- /dev/null
+++ b/docs/design/principles.md
@@ -0,0 +1,31 @@
+# 设计立场
+
+InKCre 的设计系统服务于内容的阅读、理解与操作。页面需要让人辨认信息之间的关系,知道当前处境,并看清一个动作会改变什么。文字、颜色、空间与形状的具体表达见[视觉语言](visual-language.md)。
+
+## 让内容建立秩序
+
+先决定当前任务需要呈现什么,再安排空间。移除重复、无助于理解或行动的内容与元素;有用但不属于当前层级的信息,放在可发现的后续入口。空白可以保留,不需要增加说明、状态标签或装饰去填满它。
+
+先找出用户正在理解的对象、对象之间的关系和当前任务,再给信息分组和安排操作。标题应说明下面是什么,元数据帮助定位,正文承担连续阅读。界面的分区需要与这些内容关系相符;开发时分成了几个组件,不足以决定用户应该看到几个独立容器。
+
+说明和错误也是用户完成任务所需的内容。空间不足时,先调整承载方式;为了维持一个外框而缩小或截掉必要文字,会改变信息实际受到的重视程度。
+
+## 让操作表达真实后果
+
+可执行、已选中、不可用、进行中和出错应具有可辨别的表现。用户应知道动作是否被接收、工作是否结束、失败后还能做什么。状态文字与用户输入需要连续保留,使界面的变化能够被理解,而不是让人通过控件消失或颜色变化猜测发生了什么。
+
+主要动作的强调来自当前任务,危险动作的强调来自后果。二者不因为视觉上醒目就可以互换。装饰性的变化也不应伪装成可以点击的反馈。
+
+## 让一致性保留意图和关系
+
+同样的用途与条件应有可解释的共同选择;条件不同时,允许为了同一个目的改变布局和尺度。正文可以在不同容器中有不同换行,但仍然承担正文职责。宽页面和窄侧栏可以采用不同列数,而保留同样的信息顺序与操作归属。
+
+共享某个值的依据是共同用途与共同变化理由。两个距离此刻相等,不表示它们应永远一起变化;一个场景的局部解法,也不必立即变成所有组件的配置。
+
+## 让表达的成本得到理由支持
+
+每个可见的分隔、底色、强调和层次都应帮助读者理解内容或操作。增加一个视觉处理时,说明它带来了什么区分;移除它时,检查是否真的丢失了必要信息。这个判断允许有表现力的界面,也要求表现力服务于具体内容。
+
+内容可读、功能可用与预设外观发生冲突时,先保护内容和功能,再重新选择表达方式。视觉风格需要通过这种取舍延续,不能依赖禁止变化才能成立。
+
+页面组织的具体选择见[页面组合](composition.md);实例及其可推广范围见[判断依据](reference.md)。
diff --git a/docs/design/reference.md b/docs/design/reference.md
new file mode 100644
index 0000000..b3d9bf5
--- /dev/null
+++ b/docs/design/reference.md
@@ -0,0 +1,53 @@
+# 判断依据
+
+比较设计方案时,要同时看内容、容器和状态。下面的例子分别说明字段关联、动作区别和内容增长时需要保持的关系。
+
+## 从例子提取什么
+
+先看例子面对的内容、容器和状态,再确认它保留了哪些关系,以及另一种选择会改变什么。复用有理由的关系;不要把一个截图中的全部数值、措辞和偶然布局当成规范。
+
+| 方案面对的情况 | 判断重点 | 可接受的变化 |
+| ------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------ |
+| 同一设置区块被拆成许多卡片 | 外框是否提供了新含义,还是打断字段与说明的关联 | 改用标题、对齐与区块间距;确有独立对象时保留容器 |
+| 每个区域都使用强调色或同样醒目的标题 | 用户能否区分主要内容、状态和操作 | 降低辅助信息的强调,保留必要状态和焦点 |
+| 从宽页面进入窄侧栏 | 阅读顺序、对象归属和动作是否仍然完整 | 换行、改变列数、局部滚动,不以缩字或截断必要内容换取外框不变 |
+| 多种状态与层级的信息挤在同一页面 | 当前任务是否用得上每项内容,减少后是否仍能顺利完成任务 | 删除重复信息;次要详情按需进入,当前错误与必要操作直接呈现 |
+| 新组件没有现成样板 | 能否从设计立场解释其表达,并与相邻组件共同成立 | 有依据的局部变化;新增共享角色须有共同用途 |
+
+这些是比较方案的判断方向,不是自动评分表。即便两种方案都可读、都使用合法 Token,仍需比较它们呈现的信息秩序与视觉性格。
+
+## 长字段与错误说明
+
+
+
+在 320px 宽的表单内容区域中,长标签和错误说明换行,字段随内容增长。每条说明紧邻对应字段,红色前景表达错误,标题、正文与标签保留各自的阅读职责。若为维持固定字段高度而裁切文字,用户就可能失去修正输入所需的信息。
+
+这个例子展示字段内部的关联。截图外侧留白不参与表单布局;实际页面的外部空间与字段密度按容器和内容安排。
+
+## 操作与状态的区别
+
+
+
+主要动作、辅助动作和危险动作有不同表达;pending 保留动作文字,普通 disabled 有自己的弱化表现。反馈文字在普通表面上表达不同状态,文字说明使含义不只依赖颜色。
+
+图中并列展示不同的动作和状态,便于比较,不代表实际页面的用色密度。页面按当前任务确定主要动作,并按[页面组合](composition.md)选择反馈的出现时机与强调范围;若多个动作同时获得最强强调,用户会难以辨认下一步。
+
+## 信息按任务出现
+
+来源列表先显示辨认和比较所需的名称与状态,完整配置放在详情中编辑。若同一个同步状态已经清楚表达,重复的图标、文字标签和解释不会增加信息,可以移除。连接失败时,错误原因与恢复入口进入当前视图;保存配置时,用户的修改和保存结果留在编辑上下文中。
+
+在窄容器中,可以把次要操作收进有名称的入口,但主要任务不应因此需要反复展开。进入详情再返回时保持原来的位置与选择,已打开的正文继续完整阅读。这样减少的是当前视图的负担,而不是任务所需的信息与能力。
+
+## 中西文排印
+
+中文与西文混排时,保留西文单词内部的自然字距,在汉字与西文字母、数字的交界处安排适量间距;中文标点附近不机械地补空格。标点形式与断行方式需结合文本语言及地区习惯,核对行首、行尾和不可拆开的符号组合。相关规则见 [W3C《中文排版需求》草案](https://www.w3.org/TR/clreq/)。
+
+正文的字体、字号、行距与行长应一起判断,参见 [Butterick 的正文排印指南](https://practicaltypography.com/body-text.html)。其[行长建议](https://practicaltypography.com/line-length.html)针对西文,不直接换算成中文统一字数或所有界面的固定宽度。用实际中英文段落核对换行、回行和段落节奏,短标签与连续正文分别判断。
+
+排印调整作用于显示层。用户原文、代码、URL 和标识符保持原始内容;不能为了字距或断行方便而统一改写文本。Web 的字体、样式与覆盖入口从 [README](../../README.md) 继续查阅。
+
+## 比较完整页面
+
+将两个方案放在相同的内容、容器和交互状态下,沿同一任务阅读和操作。内容阅读关注正文是否连续、元数据是否干扰;对象管理关注条目是否容易比较、操作归属是否清楚;配置修改关注字段关联、提交与反馈是否完整。
+
+调整文字、颜色或边界后,说明用户更容易辨认了什么,以及哪些信息受到弱化。再用长内容和窄容器核对这些关系是否仍然成立。局部控件的表达见[视觉语言](visual-language.md),信息和操作的组织见[页面组合](composition.md)。
diff --git a/docs/design/visual-language.md b/docs/design/visual-language.md
new file mode 100644
index 0000000..ecc7522
--- /dev/null
+++ b/docs/design/visual-language.md
@@ -0,0 +1,35 @@
+# 视觉语言
+
+InKCre 的视觉气质是平静、有秩序,略带技术感。
+
+## 无衬线为主,局部等宽混排
+
+正文、说明与普通控件使用系统 UI 无衬线字体。品牌名、少量短栏目名和编号使用局部等宽表达,通过两种字体的对比带出轻微的技术感。代码与需要逐字符辨认的技术标识也可使用等宽字体。
+
+等宽字体作用于选定的文字片段,不从页面或通用容器扩散到整组内容。普通标签、状态说明和长段文字保持无衬线表达;文本中出现英文或数字,不足以改变整段的字体。
+
+## 文字形成阅读层级
+
+标题以适度的字号差异和中等或半粗字重形成层级,正文保持常规字重。标题与正文的对比温和而明确,延续整体平静、有秩序的气质。
+
+## 颜色建立注意与状态的关系
+
+界面框架以黑、白和中性灰为主,通过明暗形成层次。基础灰保持中性,不整体偏向蓝灰或米灰。彩色出现得少,集中在需要注意的语义和动作上,让平静的版面保留明确的重点。
+
+危险动作的带色表面保持低饱和、略带灰调的质感。需要引起注意的语义文字与状态标记使用更干净、较纯的色彩,在中性框架中清晰突出。颜色的力度按承载面积与用途分别选择,不要求文字、标记与按钮底色具有相同饱和度。
+
+浅深主题都延续中性框架与少量彩色重点,按背景调整明暗对比。文字、必要边界和状态保持清晰,不通过统一降低透明度或饱和度制造柔和感。反馈的出现时机与强调范围见[页面组合](composition.md)。
+
+## 形状和层次表达边界
+
+界面以平直、利落的矩形轮廓为主,默认采用直角,整体保持薄而平面的质感。卡片、按钮、输入框和浮层遵循同一轮廓语言;组件独立或面积较小,不构成使用圆角的理由。
+
+圆角例外按明确的形态用途约定,并与相邻元素共同判断其是否协调。未有相应用途约定时延续直角;不以“柔化”“局部装饰”为由自行加入圆角。
+
+页面与常驻内容区保持平面,以细实线和轻微的底色差形成层次,默认无阴影。覆盖其他内容的菜单、弹层,在需要表达前后关系时使用小范围、低强度、贴近轮廓的阴影,并延续直角轮廓。
+
+## 空间维持节奏
+
+整体适度紧凑,局部舒展。列表与工具区形成规整的行列节奏,阅读区保留较舒展的段落节奏;较大的留白集中在段落、区块之间和页面边缘。单个字段与条目的内部空间保持适量,延续当前文字尺度。
+
+内容的取舍和分层见[页面组合](composition.md),排印与场景对照见[判断依据](reference.md)。角色名称与平台调用从 [README](../../README.md) 进入对应技术指南。
diff --git a/docs/index.md b/docs/index.md
index 3a3aa03..3f6601d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,7 +2,7 @@
## Navigation
-- [共同设计指南](../DESIGN.md)
+- [设计总纲与阅读路径](../DESIGN.md):[设计立场](design/principles.md)、[视觉语言](design/visual-language.md)、[页面组合](design/composition.md)、[判断依据](design/reference.md)
- [Repository overview](../README.md)
- [Web package usage and development](../packages/web/README.md)
- [Package migration contract](../packages/web/MIGRATION.md)
diff --git a/package.json b/package.json
index 86aa554..8d18e06 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,8 @@
"story": "pnpm --filter @inkcre/ui-web story:build",
"story:dev": "pnpm --filter @inkcre/ui-web story:dev",
"check:skills": "pnpm --filter @inkcre/ui-web skill:generate:check && pnpm --filter @inkcre/ui-web skill:validate",
- "format": "oxfmt .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md && pnpm --filter @inkcre/ui-web format",
- "format:check": "oxfmt --check .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md && pnpm --filter @inkcre/ui-web format:check",
+ "format": "oxfmt .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md docs/design && pnpm --filter @inkcre/ui-web format",
+ "format:check": "oxfmt --check .oxfmtrc.json package.json pnpm-workspace.yaml tsconfig.json scripts .github/workflows/*.yml README.md DESIGN.md docs/design && pnpm --filter @inkcre/ui-web format:check",
"lint": "oxlint --deny-warnings scripts && pnpm --filter @inkcre/ui-web lint",
"check": "pnpm format:check && pnpm lint && pnpm check:generated && pnpm --filter @inkcre/ui-web skill:validate && pnpm check:stories && pnpm check:token-workflow && pnpm type-check:root && pnpm build && pnpm check:package && pnpm story",
"changeset": "changeset",
diff --git a/packages/web/AGENTS.md b/packages/web/AGENTS.md
index 7abb208..088b752 100644
--- a/packages/web/AGENTS.md
+++ b/packages/web/AGENTS.md
@@ -6,7 +6,7 @@
运行时组件使用 Vue 3、TypeScript、Sass 和 UnoCSS。UnoCSS 同时用于图标与部分布局工具类,配置在 [uno.config.ts](uno.config.ts)。库通过 `InkRouter`、`InkI18n` 及对应注入键接入消费者的路由与国际化;Histoire 中的 vue-router/vue-i18n 配置不代表消费者必须采用相同实现。
-涉及设计、组件与 Token 时读取仓库根级 [设计指南](../../DESIGN.md)。本目录 DESIGN.md 是同源随包副本,由 Skill 生成命令同步;不要手改副本。
+涉及设计、组件与 Token 时从仓库根级 [设计总纲](../../DESIGN.md) 按任务读取正文。人工源是根级 DESIGN.md 与 docs/design;本包内相同路径均为随包副本,由 Skill 生成命令同步,不手改副本。
## 维护入口
diff --git a/packages/web/DESIGN.md b/packages/web/DESIGN.md
index 5c2d3dc..08d9ab6 100644
--- a/packages/web/DESIGN.md
+++ b/packages/web/DESIGN.md
@@ -1,77 +1,16 @@
-# InKCre 设计决策指南
+# InKCre 设计
-这份指南面向使用和维护 InKCre 设计系统的人与 Agent。它说明界面应保持的关系、默认选择,以及变化应该由谁负责。消费者用它选择和组合能力,维护者用它判断现有实现和新增能力是否合适。具体 API、安装与调用从所在仓库或安装包的 [README](README.md) 继续查阅。
+InKCre 的界面应让内容的关系和可执行的操作容易理解,让阅读、判断与修改有连续的秩序。文字、颜色、边界和空间共同表达这种秩序;具体表现随内容与容器调整,同时保留各自承担的含义。
-仓库根级文件是人工维护的源,Web 包内文件是随版本交付的副本。消费时读取安装版本的指南;维护时读取当前 checkout 的指南。指南中的 Web 能力不表示 Flutter 或 uniapp 已有对应实现。
+这里是人、消费者 Agent 和维护 Agent 的共同入口。设计知识按责任分别维护,阅读与当前任务有关的部分即可。组件 API、Web 调用和工程流程从 [README](README.md) 进入,不在这里重复。
-## 从任务决定界面
+| 当前要作出的判断 | 阅读入口 |
+| -------------------------------------------------- | ------------------------------------------ |
+| 理解共同立场,或判断一项新需求值得保持什么 | [设计立场](docs/design/principles.md) |
+| 选择文字、颜色、形状和层次,或审视整体视觉是否协调 | [视觉语言](docs/design/visual-language.md) |
+| 把内容和操作组织成页面、列表、表单或浮层 | [页面组合](docs/design/composition.md) |
+| 比较方案、解释取舍,或用已有例子校准判断 | [判断依据](docs/design/reference.md) |
-先明确用户要理解什么、改变什么,以及操作何时算完成。按这些关系组织信息、选择组件,再调整视觉。系统提供文字、颜色、空间和交互的共同约束;具体业务导航、权限、数据模型与流程由宿主产品决定。
+设计立场说明理由,视觉语言说明表达,页面组合说明关系如何落到场景,判断依据说明能从实例得出什么结论。它们共享设计方向,各自只拥有相应的知识;一个技术问题不要求加载整套正文。
-默认使用现有组件和语义角色。需要变化时,说明实际内容、容器或状态有什么不同,以及变化解决了什么问题。视觉边界与内容可读、功能可用冲突时,先保护内容与功能,再调整布局和策略。已有页面可以作为证据,但其中的偶然值或缺陷不能自动成为后续标准。
-
-## 文字与视觉层级
-
-按文字的职责选角色。正文需要支持连续阅读,标签需要标识控件或状态,说明需要表达补充信息或错误,标题需要帮助理解页面和区块结构。默认选择如下;名称对应当前系统角色,精确度量由 Token 源维护。
-
-| 内容 | 默认角色 |
-| -------------------------------------- | -------------------------------- |
-| 普通正文/说明、错误、提示 | body-md/body-sm |
-| 控件值与字段标签/短元信息、小按钮状态 | label-lg/label-md |
-| 页面标题/区块与弹层标题 | title-lg/title-sm |
-| 强调正文或引言/醒目页面标题 | body-lg/headline-lg |
-| 代码、标识符 | 合适的文字角色,加 mono 字体选择 |
-
-label-lg 和 body-sm 当前度量相同,但承担不同职责。消费者按用途选择;维护者只有在用途和未来变化责任一致时才合并角色。不能因为说明放不下,就改成更小的标签角色。
-
-默认使用系统 UI 字体,代码可使用等宽字体。家族和装饰与尺寸角色分别选择。Web 不下载字体、不重设 html 字号;相对字体尺度与比例行高表达既定依赖,不能单凭使用了 rem 就宣称所有内容都能适应。
-
-## 配色表达用途和状态
-
-颜色按文字、表面和状态的关系选择,并在实际背景上检查可读性。普通表面的文字选 text.base 或 text.subtle;primary 与 danger 表面分别配 text.on-primary、text.on-danger。反色文字的适用性取决于配对表面,不能因它叫 primary 就用于普通正文。
-
-error、success、warning、info 是普通表面上的反馈前景用途,配合明确的文字表达状态。系统目前没有约定带色反馈容器,不能从前景角色自行推导一套容器角色。必要控件边界用 border.base,装饰分隔用 border.subtle,焦点轮廓用 border.strong。
-
-hover/pressed 表达可执行动作的交互反馈;focus 表达当前键盘位置;error 表达需要处理的问题。它们的责任可以同时存在,例如错误边框不能抹掉焦点提示。pending 保留动作文字与可读配色,显示进行状态并防止重复执行;普通 disabled 使用对应的弱化表现。
-
-覆盖主题颜色时,应连同前景、背景和相关状态一起核对。某个色值自身或一种派生公式不能证明所有配对都可读。图片文字需要稳定的承载表面,不能依赖图片恰好够暗。
-
-## 空间、内容和适应
-
-先用布局、间距和对齐表达分组。字段内部、字段之间、组件内部与页面留白有不同责任,即使此刻数值相同,也不意味着必须一起变化。
-
-长内容优先换行并扩展承载区域;宿主负责页面列数、容器和外部留白,组件负责内部文字与控件的布局。带框输入的默认最小高度允许随文字增长,不应被固定高度重新裁切。无法断开的标识符、长中英文与窄容器应作为相关变化的具体核对场景。
-
-固定值、离散尺度和局部 CSS 都可以表达合理决定。按钮内与文字相关的留白可以跟随文字尺度,页面空间可以依据容器调整;不要求所有间距遵循统一缩放曲线。引入响应规则时,应说清依据哪个上下文、允许怎样变化、边界是什么,以及相对现有方案改善了什么。
-
-例如,页面在宽容器中需要更大的留白,可以由页面布局表达。只有多个场景需要共享同一种空间政策时,才考虑公共角色;一张设计稿中的数值差异不足以支持新增全局密度参数。
-
-## 组件组合与用户状态
-
-普通表单从 InkForm 的纵向布局和内置控件的 label/error 开始。Input、Textarea、Dropdown、Picker 已承担标签关联;自定义字段再用 InkField,避免重复标签。页面负责字段分组、提交和持久化;字段组件负责输入与反馈表达。
-
-按后果区分普通动作、主要动作与危险动作。主提交明确使用相应按钮主题和原生提交类型。异步操作保留动作含义和用户输入,失败应有明确反馈和可恢复路径;不能把异常显示成成功,也不能让校验默认值覆盖已有草稿。
-
-确认场景按所需语义选择:InkDoubleCheck 提供确认弹层,InkDialog 支持更完整的确认、取消与等待过程,自定义弹层才使用 InkPopup。受控模型、确认提交和取消草稿是不同责任,消费时遵循各组件参考,不从外观推断模型提交时机。
-
-加载、空结果、操作失败与确认分别表达不同状态。根据真实可达状态提供界面,不把它们合并为一个含糊的提示,也不为应用不可能进入的状态添加配置。
-
-原始编辑数据与解析结果也要区分。InkJsonEditor 编辑包含不完整 JSON 在内的原始文字;消费者只在验证通过的边界解析和持久化。适合固定业务流程的普通表单和由 schema 驱动的表单,按数据责任选择,不能仅因为后者能自动生成控件就替换前者。
-
-## 主题与实现责任
-
-用户明确选择的主题优先于系统偏好。Web 以根级主题和公开 CSS 变量提供已约定的覆盖能力;哪些值在运行时传播、哪些需要重新构建,由样式指南说明。任意 ref 值的变化不保证自动带动所有 sys 或 comp 输出。
-
-Web 的 Popup/Scrim 会移动到 body,局部容器的主题或字体不能被当成弹层的继承保证。宿主需要统一主题时,应使用已支持的根级入口。跨平台可以共享颜色用途、文字职责和状态要求;CSS、DOM、原生控件与平台交互方式由对应实现负责。
-
-## 维护和扩展
-
-修改前先确定要保持的设计关系与预期变化,再选择改动位置。复用已有角色的依据是共同用途和共同变化责任;局部计算只有形成有意支持的公共能力后才需要成为 Token 或 API。
-
-新增共享能力应说明真实使用情境、现有能力的缺口、默认行为、允许变化和覆盖责任。设计决定、输入格式、求值机制和某次渲染结果分别判断;不要把它们都当成“改一个数字”。这些说明可以放在适当的自然段和例子中,无须为每次调整填写统一模板。
-
-Token 源拥有名称、叶节点用途、类型、值与引用;组件源码和公开声明拥有 API 事实;本指南拥有跨角色与场景的设计判断。发现实现与已认可要求不一致时,应指出差异,在当前授权范围内修复或提出规则变更。不能仅为解释现有代码而修改要求。
-
-变更验证应与其影响对应:静态检查确认类型、生成与包契约,真实页面确认内容、状态、视觉和交互。生成成功不能代替设计判断。只有稳定重复、能够可靠判定的问题才值得加入自动检查;新的品牌主张或尚无证据的通用规则先作为提案讨论。
-
-对公开名称、用途、默认表现或交互承诺的改变,连同实现、指南、示例及迁移说明一起交付。Agent 的操作授权、命令和提交规则由所在仓库的 AGENTS 负责。
+仓库根级 DESIGN.md 与 docs/design 是人工维护的源,Web 包内相同路径是随版本交付的副本。消费者读取安装版本,维护者读取当前 checkout;设计意图可以跨平台延续,具体宿主业务与平台实现由各自负责。
diff --git a/packages/web/MIGRATION.md b/packages/web/MIGRATION.md
index 09b92d1..caffce9 100644
--- a/packages/web/MIGRATION.md
+++ b/packages/web/MIGRATION.md
@@ -1,23 +1,31 @@
-# Token 与样式契约升级(待发布 major)
+# 未发布:默认配色与阴影
+
+浅深主题的主动作、普通表面与 Switch 采用中性灰配对;危险按钮底色保持灰红,需要注意的反馈文字使用较纯的语义色。覆盖层阴影缩小偏移与模糊范围。公开角色、组件 API 与调用方式保持不变,升级后重新构建即可获得默认值。
+
+使用主题覆盖的消费者应一起复核前景、背景、hover/pressed 与选中状态,尤其是深色危险按钮的深色文字配对。普通状态不因成功或进行中而自动着色,使用范围见[页面组合](docs/design/composition.md)。Sass `apply-elevation` 读取运行时变量,Uno 阴影工具类使用构建值;修改根级阴影变量不会改变已经生成的 Uno 阴影,详见[样式指南](styles/README.md)。
+
+下面记录已随 2.0.0 发布的迁移要求;从 1.x 升级时同时完成。
+
+## 2.0.0:Token 与样式契约
本次升级统一了文本角色、运行时读取与状态配色。包名、组件模型和事件沿用本文后续契约,但旧文本外观和部分 Token 路径不兼容。先升级依赖及锁文件,再迁移以下调用,最后以安装产物验证页面。不要把同名角色当成旧度量的兼容别名。
-| 旧入口或用法 | 迁移选择 |
-| --- | --- |
-| `label-sm` 及 `font-label-sm`、对应 `--sys-font-label-sm-*`、Sass map | 短元信息用 label-md;正常说明/错误用 body-sm。移除 10px 默认信息角色,不提供旧别名。 |
-| 其他已有文本角色 | 名称保留,采用 rem 字号、比例行高、400 字重、0 字距;label-lg 行高从 16 调至 20。复核高度与换行。 |
-| 源/Sass map 中 `*-mono`、`lg-underlined` 复合角色 | 改为基础角色加家族/装饰;Sass `apply-font(label-lg, $mono: true, $underlined: true)`。旧 Uno mono/underlined 后缀仍接受,但采用新度量。 |
-| 字体角色的 `font-family`、`text-decoration`、`font-style/stretch`、段落缩进/间距、text-case 等变量或 map 属性 | 四属性之外不再生成。家族读取 `--sys-typo-family-sans/mono`;装饰和段落布局显式写在使用元素上。 |
-| `apply-font('title', 'lg')` | 改为 `apply-font(title-lg)`;第二参数是 mono 选项,不是字号。无效角色/选项现在编译失败。 |
-| 不存在的 body-md | 现已提供 16/24 正文;输入框用途仍优先 label-lg。新增 body-sm 和 title-lg。 |
-| `text.primary`/`text.danger-on` | 分别改为 `text.on-primary`/`text.on-danger`,仅用于对应强调表面;普通标题应选 text.base。 |
-| `text.muted` | 普通次要文字改为 text.subtle。颜色和层级有意改变。 |
-| `border.muted` | 装饰分隔用 border.subtle;必要控件边界用 border.base。base 已增强对比度。 |
-| `surface.muted` | 普通容器按层次选择 surface.base/subtle,没有无条件等价别名。 |
-| 不存在的 `color.success/warning/info.base`、`color.danger.base` | 改为 `color.feedback.success/warning/info/error`。 |
-| 不存在的 `color.success.surface`、`color.danger.surface/light` | 默认使用 surface.subtle + 对应反馈前景,不自动新增彩色容器。 |
-| 固定行高、Inter/独立 mono 字体依赖 | 检查系统 UI/mono 字体栈;库不再混用各角色字体,也不下载字体。Textarea 普通文本为 sans,代码显式 `mono`。 |
-| 覆盖 `--ref-*` 或只修改 Sass maps 期待运行时联动 | 按[样式指南](styles/README.md)覆盖 sys 家族、文本四属性、颜色、space/radius。断点等仍为构建值。 |
+| 旧入口或用法 | 迁移选择 |
+| -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| `label-sm` 及 `font-label-sm`、对应 `--sys-font-label-sm-*`、Sass map | 短元信息用 label-md;正常说明/错误用 body-sm。移除 10px 默认信息角色,不提供旧别名。 |
+| 其他已有文本角色 | 名称保留,采用 rem 字号、比例行高、400 字重、0 字距;label-lg 行高从 16 调至 20。复核高度与换行。 |
+| 源/Sass map 中 `*-mono`、`lg-underlined` 复合角色 | 改为基础角色加家族/装饰;Sass `apply-font(label-lg, $mono: true, $underlined: true)`。旧 Uno mono/underlined 后缀仍接受,但采用新度量。 |
+| 字体角色的 `font-family`、`text-decoration`、`font-style/stretch`、段落缩进/间距、text-case 等变量或 map 属性 | 四属性之外不再生成。家族读取 `--sys-typo-family-sans/mono`;装饰和段落布局显式写在使用元素上。 |
+| `apply-font('title', 'lg')` | 改为 `apply-font(title-lg)`;第二参数是 mono 选项,不是字号。无效角色/选项现在编译失败。 |
+| 不存在的 body-md | 现已提供 16/24 正文;输入框用途仍优先 label-lg。新增 body-sm 和 title-lg。 |
+| `text.primary`/`text.danger-on` | 分别改为 `text.on-primary`/`text.on-danger`,仅用于对应强调表面;普通标题应选 text.base。 |
+| `text.muted` | 普通次要文字改为 text.subtle。颜色和层级有意改变。 |
+| `border.muted` | 装饰分隔用 border.subtle;必要控件边界用 border.base。base 已增强对比度。 |
+| `surface.muted` | 普通容器按层次选择 surface.base/subtle,没有无条件等价别名。 |
+| 不存在的 `color.success/warning/info.base`、`color.danger.base` | 改为 `color.feedback.success/warning/info/error`。 |
+| 不存在的 `color.success.surface`、`color.danger.surface/light` | 默认使用 surface.subtle + 对应反馈前景,不自动新增彩色容器。 |
+| 固定行高、Inter/独立 mono 字体依赖 | 检查系统 UI/mono 字体栈;库不再混用各角色字体,也不下载字体。Textarea 普通文本为 sans,代码显式 `mono`。 |
+| 覆盖 `--ref-*` 或只修改 Sass maps 期待运行时联动 | 按[样式指南](styles/README.md)覆盖 sys 家族、文本四属性、颜色、space/radius。断点等仍为构建值。 |
颜色映射同时适用于 `--sys-color-*`、`sys-var(color, ...)`、`$color-light/dark` 的键和 Uno theme 色名;例如 `text-text-primary` 改为 `text-text-on-primary`。不保留会掩盖语义变化的双套颜色名。Uno theme.colors 的复合键按 Wind3 解析规则使用 camelCase(如 onPrimary、dangerHover);工具类仍为 text-text-on-primary、bg-surface-danger-hover。直接读取 preset theme 的代码也需迁移。
@@ -31,9 +39,9 @@ Figma 导入也改变:仅更新已知路径的值,显式提供 releaseType
---
-# 组件基础契约升级(待发布 major)
+## 2.0.0:组件基础契约
-本节对应基础控件、弹层和 schema 编辑契约调整,尚未发布。选择包含这些变更的版本前,完成以下迁移并验证真实保存流程。
+本节对应 2.0.0 的基础控件、弹层和 schema 编辑契约调整。升级时完成以下迁移并验证真实保存流程。
- InkButton 默认原生类型改为 button。表单提交写 `nativeType="submit"`;type 仍控制 default/square 外形。client-web 的 info-base/list 和 RecallSearch 搜索按钮依赖旧默认提交行为,需要明确标记。
- InkJsonEditor 的模型现在包含原始文本和无效 JSON。将直接 JSON.parse 的 computed setter 改为独立字符串草稿,监听 validation;仅在 valid=true 且结果 text 与草稿一致时解析和持久化。client-web 的 peerCard 配置编辑属于这种调用,必须迁移,否则输入中间态会让 setter 抛错。
@@ -56,7 +64,11 @@ function save() {
-
+
```
diff --git a/packages/web/README.md b/packages/web/README.md
index 31b8bd1..5bd85e0 100644
--- a/packages/web/README.md
+++ b/packages/web/README.md
@@ -18,7 +18,7 @@ JSON 语言服务当前固定为 5.6.4:组件依赖该版本的诊断消息和
## 设计与使用入口
-构建、审视或维护 UI 时,先读 [DESIGN.md](DESIGN.md) 的默认选择和设计边界,再读相关组件参考。此文件随当前包版本交付;生产者仓库中的未来版本规则不自动适用于旧版安装。
+构建、审视或维护 UI 时,从 [DESIGN.md](DESIGN.md) 的总纲进入,按任务读取设计立场、视觉语言、页面组合或判断依据,再查所需组件参考。无需预先加载全部设计正文。总纲与 docs/design 一起随当前包版本交付,生产者未来版本的规则不自动适用于旧版安装。
应用入口加载样式;需要全局注册所有组件时安装插件:
diff --git a/packages/web/docs/design/composition.md b/packages/web/docs/design/composition.md
new file mode 100644
index 0000000..e6af3cb
--- /dev/null
+++ b/packages/web/docs/design/composition.md
@@ -0,0 +1,51 @@
+# 页面组合
+
+本页说明如何让内容、操作和状态在页面中形成关系。[设计立场](principles.md) 说明理由,[视觉语言](visual-language.md) 说明表达;组件的参数和模型时机从 [README](../../README.md) 进入对应参考。
+
+## 从完整任务安排阅读顺序
+
+先让用户辨认正在面对的对象,再安排理解它所需的信息、当前状态和下一步动作。标题、摘要、元数据与操作之间的主次应清楚;元数据接近其解释的内容,局部操作接近其作用对象,页面级操作则有稳定的位置。
+
+页面可以同时容纳阅读和管理,但不必让每一项信息争夺同样的注意。普通列表、内容正文和设置表单的节奏可以不同,宿主负责根据真实任务决定导航与业务流程。
+
+## 决定当前呈现什么
+
+整理内容先于调整间距。重复的标题、复述同一状态的标签、没有帮助的说明和纯粹填空的图标应直接移除;需要保留的内容,再按当前任务决定出现的位置与时机。已有空间不要求被填满。
+
+列表帮助识别和比较对象,详情承载进一步阅读,编辑态提供修改所需的信息。不要把三个层级的全部内容同时摆在列表里。有用但暂时次要的信息可放到展开区、详情或次级操作入口中,入口应可发现,并让人知道能在那里找到什么。若用户必须反复展开才能完成主要任务,就需要重新考虑哪些内容应直接出现。
+
+界面随状态改变当前呈现的内容:保存期间保留输入与进度,失败后及时显示对应问题和恢复动作,无须同时陈列所有可能状态。影响当前判断的警告、校验错误和操作后果直接呈现;不能为了视觉简洁藏到容易错过的入口。
+
+状态类别不自动决定它必须着色。常态的“已连接”、例行保存结果与普通进度说明默认使用中性色;当结果需要特别确认、风险需要处理或变化会影响下一步时,再用语义色强调关键文字或标记,详细说明保持普通文字的阅读层级。多个问题同时存在时,按影响范围与处理优先级组织,不为减少彩色而隐藏必要反馈。
+
+容器变窄时,重新安排次要内容与操作的入口,保留主要任务及必要信息的可达性。切换尺寸、进入详情和返回列表时,草稿、选择与当前上下文应能延续。用完整操作过程检查这些变化,避免只让默认状态或一个尺寸显得简洁。
+
+## 分组与独立容器
+
+连续的说明、属于同一对象的字段、围绕同一结果的元数据,先通过顺序、对齐和间距形成一组。只有当封闭边界进一步说明了独立对象、交互范围或层次时,才增加容器。
+
+例如,一个设置区块可以由标题、说明和纵向字段直接组成。给每个字段再包一层卡片,容易打断整组阅读,并让辅助说明看起来像另一个对象。相反,多个独立来源各有名称、状态和操作时,重复的条目或容器可以帮助辨认对象归属。应该比较这些关系,而不是统一要求所有内容使用或不用卡片。
+
+列表条目具有共同结构时,通过稳定的对齐与顺序帮助比较;需要连续阅读的正文应保留完整内容。摘要是进入全文前的选择帮助,不能继续截断已打开的完整内容。
+
+## 表单围绕修改与提交组织
+
+普通表单从纵向字段开始。标签靠近输入,说明与错误放在相关字段旁边;字段分组表达业务含义,不按底层组件类型分类。自带 label/error 的控件已经承担关联,自定义字段再补对应结构。
+
+主要提交应能被辨认为这次修改的完成动作,取消和危险操作与其保持可理解的区别。校验失败应说明需要修改什么;保存失败应保留已经输入的内容,让重试有连续的上下文。保存期间保留动作文字与可读配色,并防止重复执行。
+
+固定业务流程与 schema 生成表单承担不同的数据责任。先决定用户如何理解和修改内容,再选择实现;具体组合从 Web 的 InkForm、内置字段、InkField 以及现有配方进入。
+
+## 浮层延续当前任务
+
+确认需要让用户看清对象、后果和选择;内容浮层需要让人理解正在阅读什么、如何继续或返回。浮层内部不应因为处于弹层中就再重复一套页面外壳。
+
+普通确认、需要等待的确认和自定义内容浮层各有适用语义。Web 的具体选择由 InkDoubleCheck、InkDialog、InkPopup 的参考说明。背景是否继续可操作、何时提交和何时放弃草稿,都应与当前任务一致,不能从外观推断。
+
+加载、空结果、失败与确认有不同含义,应分别表达。错误反馈不要抹掉键盘焦点,进行状态也不应伪装成普通不可用状态。
+
+## 容器变化时保留关系
+
+宿主决定页面列数、外部留白和阅读区域,组件负责内部内容与操作布局。确定当前需要呈现的内容后,可用空间缩小时让相关内容换行或按既有阅读顺序排列;不要通过缩小正文、遮掉必要操作或裁切说明来维持双栏外观。
+
+同一界面在宽屏与窄容器中可以看起来不同,但对象归属、信息主次和动作含义应能被追踪。标识符可以换行,结构化代码可以局部滚动;局部滚动不应吞掉整个页面的操作入口。具体对照见[判断依据](reference.md)。
diff --git a/packages/web/docs/design/examples/action-states.png b/packages/web/docs/design/examples/action-states.png
new file mode 100644
index 0000000..6a11bff
Binary files /dev/null and b/packages/web/docs/design/examples/action-states.png differ
diff --git a/packages/web/docs/design/examples/form-content.png b/packages/web/docs/design/examples/form-content.png
new file mode 100644
index 0000000..6807bdc
Binary files /dev/null and b/packages/web/docs/design/examples/form-content.png differ
diff --git a/packages/web/docs/design/principles.md b/packages/web/docs/design/principles.md
new file mode 100644
index 0000000..644b197
--- /dev/null
+++ b/packages/web/docs/design/principles.md
@@ -0,0 +1,31 @@
+# 设计立场
+
+InKCre 的设计系统服务于内容的阅读、理解与操作。页面需要让人辨认信息之间的关系,知道当前处境,并看清一个动作会改变什么。文字、颜色、空间与形状的具体表达见[视觉语言](visual-language.md)。
+
+## 让内容建立秩序
+
+先决定当前任务需要呈现什么,再安排空间。移除重复、无助于理解或行动的内容与元素;有用但不属于当前层级的信息,放在可发现的后续入口。空白可以保留,不需要增加说明、状态标签或装饰去填满它。
+
+先找出用户正在理解的对象、对象之间的关系和当前任务,再给信息分组和安排操作。标题应说明下面是什么,元数据帮助定位,正文承担连续阅读。界面的分区需要与这些内容关系相符;开发时分成了几个组件,不足以决定用户应该看到几个独立容器。
+
+说明和错误也是用户完成任务所需的内容。空间不足时,先调整承载方式;为了维持一个外框而缩小或截掉必要文字,会改变信息实际受到的重视程度。
+
+## 让操作表达真实后果
+
+可执行、已选中、不可用、进行中和出错应具有可辨别的表现。用户应知道动作是否被接收、工作是否结束、失败后还能做什么。状态文字与用户输入需要连续保留,使界面的变化能够被理解,而不是让人通过控件消失或颜色变化猜测发生了什么。
+
+主要动作的强调来自当前任务,危险动作的强调来自后果。二者不因为视觉上醒目就可以互换。装饰性的变化也不应伪装成可以点击的反馈。
+
+## 让一致性保留意图和关系
+
+同样的用途与条件应有可解释的共同选择;条件不同时,允许为了同一个目的改变布局和尺度。正文可以在不同容器中有不同换行,但仍然承担正文职责。宽页面和窄侧栏可以采用不同列数,而保留同样的信息顺序与操作归属。
+
+共享某个值的依据是共同用途与共同变化理由。两个距离此刻相等,不表示它们应永远一起变化;一个场景的局部解法,也不必立即变成所有组件的配置。
+
+## 让表达的成本得到理由支持
+
+每个可见的分隔、底色、强调和层次都应帮助读者理解内容或操作。增加一个视觉处理时,说明它带来了什么区分;移除它时,检查是否真的丢失了必要信息。这个判断允许有表现力的界面,也要求表现力服务于具体内容。
+
+内容可读、功能可用与预设外观发生冲突时,先保护内容和功能,再重新选择表达方式。视觉风格需要通过这种取舍延续,不能依赖禁止变化才能成立。
+
+页面组织的具体选择见[页面组合](composition.md);实例及其可推广范围见[判断依据](reference.md)。
diff --git a/packages/web/docs/design/reference.md b/packages/web/docs/design/reference.md
new file mode 100644
index 0000000..b3d9bf5
--- /dev/null
+++ b/packages/web/docs/design/reference.md
@@ -0,0 +1,53 @@
+# 判断依据
+
+比较设计方案时,要同时看内容、容器和状态。下面的例子分别说明字段关联、动作区别和内容增长时需要保持的关系。
+
+## 从例子提取什么
+
+先看例子面对的内容、容器和状态,再确认它保留了哪些关系,以及另一种选择会改变什么。复用有理由的关系;不要把一个截图中的全部数值、措辞和偶然布局当成规范。
+
+| 方案面对的情况 | 判断重点 | 可接受的变化 |
+| ------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------ |
+| 同一设置区块被拆成许多卡片 | 外框是否提供了新含义,还是打断字段与说明的关联 | 改用标题、对齐与区块间距;确有独立对象时保留容器 |
+| 每个区域都使用强调色或同样醒目的标题 | 用户能否区分主要内容、状态和操作 | 降低辅助信息的强调,保留必要状态和焦点 |
+| 从宽页面进入窄侧栏 | 阅读顺序、对象归属和动作是否仍然完整 | 换行、改变列数、局部滚动,不以缩字或截断必要内容换取外框不变 |
+| 多种状态与层级的信息挤在同一页面 | 当前任务是否用得上每项内容,减少后是否仍能顺利完成任务 | 删除重复信息;次要详情按需进入,当前错误与必要操作直接呈现 |
+| 新组件没有现成样板 | 能否从设计立场解释其表达,并与相邻组件共同成立 | 有依据的局部变化;新增共享角色须有共同用途 |
+
+这些是比较方案的判断方向,不是自动评分表。即便两种方案都可读、都使用合法 Token,仍需比较它们呈现的信息秩序与视觉性格。
+
+## 长字段与错误说明
+
+
+
+在 320px 宽的表单内容区域中,长标签和错误说明换行,字段随内容增长。每条说明紧邻对应字段,红色前景表达错误,标题、正文与标签保留各自的阅读职责。若为维持固定字段高度而裁切文字,用户就可能失去修正输入所需的信息。
+
+这个例子展示字段内部的关联。截图外侧留白不参与表单布局;实际页面的外部空间与字段密度按容器和内容安排。
+
+## 操作与状态的区别
+
+
+
+主要动作、辅助动作和危险动作有不同表达;pending 保留动作文字,普通 disabled 有自己的弱化表现。反馈文字在普通表面上表达不同状态,文字说明使含义不只依赖颜色。
+
+图中并列展示不同的动作和状态,便于比较,不代表实际页面的用色密度。页面按当前任务确定主要动作,并按[页面组合](composition.md)选择反馈的出现时机与强调范围;若多个动作同时获得最强强调,用户会难以辨认下一步。
+
+## 信息按任务出现
+
+来源列表先显示辨认和比较所需的名称与状态,完整配置放在详情中编辑。若同一个同步状态已经清楚表达,重复的图标、文字标签和解释不会增加信息,可以移除。连接失败时,错误原因与恢复入口进入当前视图;保存配置时,用户的修改和保存结果留在编辑上下文中。
+
+在窄容器中,可以把次要操作收进有名称的入口,但主要任务不应因此需要反复展开。进入详情再返回时保持原来的位置与选择,已打开的正文继续完整阅读。这样减少的是当前视图的负担,而不是任务所需的信息与能力。
+
+## 中西文排印
+
+中文与西文混排时,保留西文单词内部的自然字距,在汉字与西文字母、数字的交界处安排适量间距;中文标点附近不机械地补空格。标点形式与断行方式需结合文本语言及地区习惯,核对行首、行尾和不可拆开的符号组合。相关规则见 [W3C《中文排版需求》草案](https://www.w3.org/TR/clreq/)。
+
+正文的字体、字号、行距与行长应一起判断,参见 [Butterick 的正文排印指南](https://practicaltypography.com/body-text.html)。其[行长建议](https://practicaltypography.com/line-length.html)针对西文,不直接换算成中文统一字数或所有界面的固定宽度。用实际中英文段落核对换行、回行和段落节奏,短标签与连续正文分别判断。
+
+排印调整作用于显示层。用户原文、代码、URL 和标识符保持原始内容;不能为了字距或断行方便而统一改写文本。Web 的字体、样式与覆盖入口从 [README](../../README.md) 继续查阅。
+
+## 比较完整页面
+
+将两个方案放在相同的内容、容器和交互状态下,沿同一任务阅读和操作。内容阅读关注正文是否连续、元数据是否干扰;对象管理关注条目是否容易比较、操作归属是否清楚;配置修改关注字段关联、提交与反馈是否完整。
+
+调整文字、颜色或边界后,说明用户更容易辨认了什么,以及哪些信息受到弱化。再用长内容和窄容器核对这些关系是否仍然成立。局部控件的表达见[视觉语言](visual-language.md),信息和操作的组织见[页面组合](composition.md)。
diff --git a/packages/web/docs/design/visual-language.md b/packages/web/docs/design/visual-language.md
new file mode 100644
index 0000000..ecc7522
--- /dev/null
+++ b/packages/web/docs/design/visual-language.md
@@ -0,0 +1,35 @@
+# 视觉语言
+
+InKCre 的视觉气质是平静、有秩序,略带技术感。
+
+## 无衬线为主,局部等宽混排
+
+正文、说明与普通控件使用系统 UI 无衬线字体。品牌名、少量短栏目名和编号使用局部等宽表达,通过两种字体的对比带出轻微的技术感。代码与需要逐字符辨认的技术标识也可使用等宽字体。
+
+等宽字体作用于选定的文字片段,不从页面或通用容器扩散到整组内容。普通标签、状态说明和长段文字保持无衬线表达;文本中出现英文或数字,不足以改变整段的字体。
+
+## 文字形成阅读层级
+
+标题以适度的字号差异和中等或半粗字重形成层级,正文保持常规字重。标题与正文的对比温和而明确,延续整体平静、有秩序的气质。
+
+## 颜色建立注意与状态的关系
+
+界面框架以黑、白和中性灰为主,通过明暗形成层次。基础灰保持中性,不整体偏向蓝灰或米灰。彩色出现得少,集中在需要注意的语义和动作上,让平静的版面保留明确的重点。
+
+危险动作的带色表面保持低饱和、略带灰调的质感。需要引起注意的语义文字与状态标记使用更干净、较纯的色彩,在中性框架中清晰突出。颜色的力度按承载面积与用途分别选择,不要求文字、标记与按钮底色具有相同饱和度。
+
+浅深主题都延续中性框架与少量彩色重点,按背景调整明暗对比。文字、必要边界和状态保持清晰,不通过统一降低透明度或饱和度制造柔和感。反馈的出现时机与强调范围见[页面组合](composition.md)。
+
+## 形状和层次表达边界
+
+界面以平直、利落的矩形轮廓为主,默认采用直角,整体保持薄而平面的质感。卡片、按钮、输入框和浮层遵循同一轮廓语言;组件独立或面积较小,不构成使用圆角的理由。
+
+圆角例外按明确的形态用途约定,并与相邻元素共同判断其是否协调。未有相应用途约定时延续直角;不以“柔化”“局部装饰”为由自行加入圆角。
+
+页面与常驻内容区保持平面,以细实线和轻微的底色差形成层次,默认无阴影。覆盖其他内容的菜单、弹层,在需要表达前后关系时使用小范围、低强度、贴近轮廓的阴影,并延续直角轮廓。
+
+## 空间维持节奏
+
+整体适度紧凑,局部舒展。列表与工具区形成规整的行列节奏,阅读区保留较舒展的段落节奏;较大的留白集中在段落、区块之间和页面边缘。单个字段与条目的内部空间保持适量,延续当前文字尺度。
+
+内容的取舍和分层见[页面组合](composition.md),排印与场景对照见[判断依据](reference.md)。角色名称与平台调用从 [README](../../README.md) 进入对应技术指南。
diff --git a/packages/web/histoire.config.ts b/packages/web/histoire.config.ts
index 620e398..2c906ee 100644
--- a/packages/web/histoire.config.ts
+++ b/packages/web/histoire.config.ts
@@ -1,185 +1,6 @@
import { defineConfig } from "histoire";
import { HstVue } from "@histoire/plugin-vue";
import { resolve } from "path";
-import { readFileSync, writeFileSync } from "fs";
-import type { Plugin } from "vite";
-import MarkdownIt from "markdown-it";
-
-const cdnModules = {
- "@codemirror/view": "https://esm.sh/@codemirror/view@6.38.8",
- "@codemirror/state": "https://esm.sh/@codemirror/state@6.5.2",
- "@codemirror/lang-json": "https://esm.sh/@codemirror/lang-json@6.0.2",
- "@codemirror/commands": "https://esm.sh/@codemirror/commands@6.10.0",
- "@codemirror/autocomplete": "https://esm.sh/@codemirror/autocomplete@6.20.0",
- "@codemirror/lint": "https://esm.sh/@codemirror/lint@6.9.2",
- "vscode-json-languageservice": "https://esm.sh/vscode-json-languageservice@5.6.4",
- "vscode-languageserver-textdocument": "https://esm.sh/vscode-languageserver-textdocument@1.0.12",
- // Also externalize Vue and related libraries
- // Note: Versions match peerDependencies in package.json
- // This is only for Histoire build (dev tool), not production builds
- vue: "https://esm.sh/vue@3.5.25",
- "vue-router": "https://esm.sh/vue-router@4.6.4",
- "@vueuse/core": "https://esm.sh/@vueuse/core@14.1.0",
- dayjs: "https://esm.sh/dayjs@1.11.19",
-};
-
-const cdnExternalsPlugin = (): Plugin => {
- let isBuild = false;
- let outDir = "";
- let htmlTransformed = false;
-
- return {
- name: "cdn-externals",
- enforce: "pre",
-
- configResolved(config) {
- isBuild = config.command === "build";
- outDir = config.build.outDir;
- },
-
- resolveId(id) {
- if (isBuild && Object.keys(cdnModules).includes(id)) {
- // Return a special virtual module ID
- return "\0" + id;
- }
- return null;
- },
-
- load(id) {
- if (isBuild && id.startsWith("\0")) {
- const originalId = id.slice(1);
- if (originalId in cdnModules) {
- const cdnUrl = cdnModules[originalId as keyof typeof cdnModules];
- // Return a proxy module that imports from CDN
- return `export * from '${cdnUrl}';`;
- }
- }
- return null;
- },
-
- closeBundle() {
- if (isBuild && !htmlTransformed) {
- try {
- const htmlPath = resolve(outDir, "index.html");
-
- // Check if file exists
- try {
- readFileSync(htmlPath, "utf-8");
- } catch {
- // HTML file doesn't exist yet, skip transformation
- return;
- }
-
- let html = readFileSync(htmlPath, "utf-8");
-
- // Check if import map already exists using regex
- if (/\n`;
-
- html = html.replace(/<\/head>/i, importMapScript + "");
- writeFileSync(htmlPath, html);
- htmlTransformed = true;
- console.log("[CDN] Added import map to HTML");
- } catch (err) {
- console.error("[CDN] Failed to transform HTML:", err);
- }
- }
- },
- };
-};
-
-/**
- * Vite plugin to optimize Shiki bundle in different modes
- * - Dev mode: Only allow specific languages (vue, typescript, javascript) by intercepting imports
- * - Build mode: Exclude Shiki completely
- */
-const optimizeShikiPlugin = (): Plugin => {
- let isBuild = false;
- // Include both full names and common aliases for language matching
- const allowedLangs = ["vue", "typescript", "javascript", "ts", "js"];
-
- // Helper function to extract module name from import path
- const extractModuleName = (id: string, prefix: string): string => {
- return id.replace(prefix, "").replace(/\.(mjs|js|ts)$/, "");
- };
-
- return {
- name: "optimize-shiki",
- enforce: "pre",
-
- configResolved(config) {
- isBuild = config.command === "build";
- },
-
- resolveId(id) {
- if (isBuild) {
- // In build mode, block all Shiki-related imports
- if (id.includes("shiki") || id.includes("@shikijs")) {
- console.log(`[Optimize Shiki] Blocking in build mode: ${id}`);
- return "\0shiki-excluded";
- }
- } else {
- // In dev mode, block non-essential language imports
- // Allow index files and only specific language files
- const isLangsIndex = id === "@shikijs/langs" || id.includes("/langs/dist/index.mjs");
- if (id.startsWith("@shikijs/langs/") && !isLangsIndex) {
- const langName = extractModuleName(id, "@shikijs/langs/");
- if (!allowedLangs.includes(langName)) {
- // Block this language
- return "\0shiki-lang-blocked";
- }
- }
- // Block non-essential theme imports (only allow github-light)
- // Allow index files and only the github-light theme
- const isThemesIndex = id === "@shikijs/themes" || id.includes("/themes/dist/index.mjs");
- if (id.startsWith("@shikijs/themes/") && !isThemesIndex) {
- const themeName = extractModuleName(id, "@shikijs/themes/");
- if (themeName !== "github-light") {
- // Block this theme
- return "\0shiki-theme-blocked";
- }
- }
- }
- return null;
- },
-
- load(id) {
- if (id === "\0shiki-lang-blocked" || id === "\0shiki-theme-blocked") {
- // Return empty module for blocked languages/themes in dev mode
- return `export default null;`;
- }
-
- if (id === "\0shiki-excluded") {
- // Build mode: Return empty module for all Shiki
- return `
-// Shiki excluded in build mode - using plain markdown rendering
-export default {};
-export const getHighlighter = () => Promise.resolve({});
-export const createHighlighter = () => Promise.resolve({});
-export const bundledLanguages = {};
-export const bundledThemes = {};
-export const codeToHtml = () => Promise.resolve('');
- `;
- }
- return null;
- },
- };
-};
-
export default defineConfig({
plugins: [HstVue()],
viteIgnorePlugins: ["vite:dts"],
@@ -219,28 +40,10 @@ export default defineConfig({
contrastColor: "#fafafa",
},
],
- // Configure markdown rendering
- // In dev mode: Use Histoire's default (which will use our optimized Shiki with limited langs/themes)
- // In build mode: Use plain markdown-it (Shiki excluded by plugin)
- // Note: Using process.env.NODE_ENV here is correct as Histoire sets it based on command
- markdown:
- process.env.NODE_ENV === "production"
- ? (_env) => {
- // Build mode: Use markdown-it without Shiki
- const md = new MarkdownIt({
- html: true,
- linkify: true,
- typographer: true,
- });
- // Code blocks will render as plain
...
- return md;
- }
- : undefined, // Dev mode: Use Histoire's default with our optimized Shiki
vite: {
resolve: {
alias: [{ find: /^@inkcre\/ui-web$/, replacement: resolve(__dirname, "src/index.ts") }],
},
- plugins: [optimizeShikiPlugin(), cdnExternalsPlugin()],
build: {
sourcemap: false,
minify: "terser",
diff --git a/packages/web/package.json b/packages/web/package.json
index 54d48f7..7dbc576 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -20,7 +20,8 @@
"styles/",
"skills",
"MIGRATION.md",
- "DESIGN.md"
+ "DESIGN.md",
+ "docs/design/"
],
"type": "module",
"sideEffects": [
@@ -85,8 +86,8 @@
"build:skills": "pnpm run skill:generate:check",
"clean": "rimraf dist",
"preview": "vite preview",
- "format": "oxfmt --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json DESIGN.md skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json tsconfig.uno.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js",
- "format:check": "oxfmt --check --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json DESIGN.md skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json tsconfig.uno.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js",
+ "format": "oxfmt --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json DESIGN.md docs/design skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json tsconfig.uno.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js",
+ "format:check": "oxfmt --check --config .oxfmtrc.json src stories styles .oxfmtrc.json .oxlintrc.json package.json DESIGN.md docs/design skill.seed.json component-manifest.json component-manifest.schema.json tsconfig.json tsconfig.build.json tsconfig.check.json tsconfig.exports.json tsconfig.uno.json env.d.ts histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js",
"lint": "oxlint --config .oxlintrc.json src stories histoire.config.ts uno.config.ts vite.config.js vite.subpaths.config.js",
"lint:fix": "pnpm run lint --fix",
"skill:generate": "tsx ../../scripts/build-agent-skills.ts",
diff --git a/packages/web/skill.seed.json b/packages/web/skill.seed.json
index 582c2ca..2b9997f 100644
--- a/packages/web/skill.seed.json
+++ b/packages/web/skill.seed.json
@@ -465,7 +465,7 @@
"使用 InkForm layout=\"col\",内置控件直接传 label、error,保留各自模型名称;自定义字段才包 InkField。",
"标题选择 title-lg,正文 body-md,说明和错误 body-sm;短元信息才选 label-md。页面留白和列数写在宿主 CSS 中。",
"主提交写 InkButton theme=\"primary\" native-type=\"submit\",传 isLoading;业务代码处理校验、异步失败和防止重复提交。",
- "成功/失败写明确文字,默认使用 surface.subtle 搭配 feedback.success/error;不要创建不存在的 success.surface 或 danger.light。"
+ "成功/失败写明确文字,例行保存结果和普通进度使用 text.base 或 text.subtle;需要强调的结果、风险或变化才使用对应 feedback 前景。不要创建不存在的 success.surface 或 danger.light;出现时机见设计指南的页面组合。"
],
"caveats": [
"Do not normalize every control to the same model prop name; use each component's actual API.",
@@ -594,11 +594,11 @@
"styling": {
"rules": [
"应用加载一次 @inkcre/ui-web/styles;Sass 使用公开 /styles/functions、/styles/mixins 和 /tokens/ref|sys|comp。",
- "设计角色的选择、配色关系、状态与布局责任见包内 DESIGN.md;CSS/Sass/Uno 调用与覆盖细节见 styles/README.md。",
+ "包内 DESIGN.md 提供总纲;按任务读取 docs/design 中的视觉语言或页面组合,比较方案时再读判断依据。CSS/Sass/Uno 调用与覆盖细节见 styles/README.md。",
"apply-font(body-md) 输出四个度量、系统 UI 字体和 none 装饰;代码加 $mono: true,下划线加 $underlined: true。apply-font(title, lg) 无效,应使用 title-lg。",
"Uno 将 presetInk 放在 presetWind3 后;font-body-md、font-label-lg font-mono underline、p-md 使用同一公开角色。none 不能消除祖先绘制的下划线。",
"html[data-theme=\"light|dark\"] 优先于系统;省略 data-theme 或设 system 时跟随系统。主题覆盖 CSS 放在库之后并匹配选择器。",
- "根级 sys 颜色、typo.family.sans/mono、font 角色四属性、space/radius 在 CSS/Sass 变量和 Uno 对应规则之间传播。Sass maps 与 Uno 尺寸/图标/断点/阴影是构建值;任意 ref 变化不保证带动 sys/comp。",
+ "根级 sys 颜色、typo.family.sans/mono、font 角色四属性、space/radius 在 CSS/Sass 变量和 Uno 对应规则之间传播。apply-elevation 读取运行时 sys.elevation.raised 变量;Sass maps 与 Uno 尺寸/图标/断点/阴影是构建值,任意 ref 变化不保证带动 sys/comp。",
"Popup/Scrim Teleport 到 body,只保证根级覆盖,局部 wrapper 的主题或字体不能自动跟随。库不设置 html 字号,不下载字体。",
"构建 CSS 自带组件内部静态图标和工具类,不含 preflight;应用动态 icon 仍需自身 Uno 提取或 safelist。"
],
diff --git a/packages/web/skills/ui-web/SKILL.md b/packages/web/skills/ui-web/SKILL.md
index 8c60857..c2e6a95 100644
--- a/packages/web/skills/ui-web/SKILL.md
+++ b/packages/web/skills/ui-web/SKILL.md
@@ -16,7 +16,7 @@ matches a product task.
## Workflow
1. Identify the product intent and interaction state.
-2. Read [DESIGN.md](../../DESIGN.md) for design decisions, defaults, and extension boundaries; then read `references/component-map.md` to shortlist components.
+2. 从 [DESIGN.md](../../DESIGN.md) 的总纲按当前任务读取设计正文;无需加载全部文件,再用 `references/component-map.md` 选择组件。
3. For multi-component work, read `references/composition-recipes.md`.
4. Load only the selected files under `references/components/`.
5. Check integration, styling, and common-mistake references only when relevant.
@@ -64,7 +64,7 @@ app.use(InKCreUiWeb);
## References
-- [DESIGN.md](../../DESIGN.md): shared design decisions for consumers and maintainers.
+- [DESIGN.md](../../DESIGN.md): 设计总纲与按任务阅读的路径;正文分别说明立场、视觉、组合和判断依据。
- `references/component-map.md`: intent-to-component routing.
- `references/composition-recipes.md`: reviewed multi-component workflows.
- `references/integration.md`: installation, router, and i18n boundaries.
diff --git a/packages/web/skills/ui-web/references/composition-recipes.md b/packages/web/skills/ui-web/references/composition-recipes.md
index 20d7e5e..783d13e 100644
--- a/packages/web/skills/ui-web/references/composition-recipes.md
+++ b/packages/web/skills/ui-web/references/composition-recipes.md
@@ -14,7 +14,7 @@ current generated API facts used to implement them.
1. 使用 InkForm layout="col",内置控件直接传 label、error,保留各自模型名称;自定义字段才包 InkField。
2. 标题选择 title-lg,正文 body-md,说明和错误 body-sm;短元信息才选 label-md。页面留白和列数写在宿主 CSS 中。
3. 主提交写 InkButton theme="primary" native-type="submit",传 isLoading;业务代码处理校验、异步失败和防止重复提交。
-4. 成功/失败写明确文字,默认使用 surface.subtle 搭配 feedback.success/error;不要创建不存在的 success.surface 或 danger.light。
+4. 成功/失败写明确文字,例行保存结果和普通进度使用 text.base 或 text.subtle;需要强调的结果、风险或变化才使用对应 feedback 前景。不要创建不存在的 success.surface 或 danger.light;出现时机见设计指南的页面组合。
```vue
-
+
+
diff --git a/packages/web/stories/controls/inkSwitch.story.md b/packages/web/stories/controls/inkSwitch.story.md
index 382a13f..38a586a 100644
--- a/packages/web/stories/controls/inkSwitch.story.md
+++ b/packages/web/stories/controls/inkSwitch.story.md
@@ -6,4 +6,4 @@ modelValue 可以是 boolean 或 Promise。Promise 期间保留上一
受控模型仍由父级接受 `update:modelValue`,组件不会绕过父级拒绝的更新。
-轨道由两个等宽区域组成,两种状态文案共同参与尺寸计算,切换与 pending 不改变宽度。xs/sm/md/lg 最小尺寸为 60×24、80×24、100×36、120×48px,字体放大或长文案可增大尺寸。检查“长状态文案保持轨道尺寸”中的英文、中文和等待状态。
+普通状态文案使用系统 UI 字体。轨道由两个等宽区域组成,两种状态文案共同参与尺寸计算,切换与 pending 不改变宽度。xs/sm/md/lg 最小尺寸为 60×24、80×24、100×36、120×48px,字体放大或长文案可增大尺寸。检查“长状态文案保持轨道尺寸”中的英文、中文和等待状态。
diff --git a/packages/web/stories/forms/inkAutoForm.story.vue b/packages/web/stories/forms/inkAutoForm.story.vue
index e6e619c..63e764c 100644
--- a/packages/web/stories/forms/inkAutoForm.story.vue
+++ b/packages/web/stories/forms/inkAutoForm.story.vue
@@ -315,18 +315,21 @@ const retainedSchema: JSONSchema = {
@validation="retainedValidation = $event"
/>
校验:{{ retainedValidation?.status }}
- {{ retainedData }}
+ {{ retainedData }}
-
diff --git a/packages/web/stories/recipes/SettingsForm.vue b/packages/web/stories/recipes/SettingsForm.vue
index 5a65389..b513436 100644
--- a/packages/web/stories/recipes/SettingsForm.vue
+++ b/packages/web/stories/recipes/SettingsForm.vue
@@ -63,6 +63,6 @@ async function submit() {
line-height: var(--sys-font-body-sm-line-height);
font-weight: var(--sys-font-body-sm-font-weight);
letter-spacing: var(--sys-font-body-sm-letter-spacing);
- color: var(--sys-color-feedback-success);
+ color: var(--sys-color-text-subtle);
}
diff --git a/packages/web/styles/README.md b/packages/web/styles/README.md
index 0b0a0ca..397a41e 100644
--- a/packages/web/styles/README.md
+++ b/packages/web/styles/README.md
@@ -4,9 +4,21 @@
## 从设计选择到 Web 调用
-文字角色、配色组合、状态和布局责任见 [设计指南](../DESIGN.md)。本指南负责把这些决定映射到 CSS、Sass 和 UnoCSS,以及说明覆盖在哪里生效。
+文字、配色与空间关系见[视觉语言](../docs/design/visual-language.md),页面组织见[页面组合](../docs/design/composition.md);总纲与阅读路径见 [DESIGN.md](../DESIGN.md)。本指南负责把选定角色映射到 CSS、Sass 和 UnoCSS,以及说明覆盖在哪里生效。
-常见正文使用 body-md,说明使用 body-sm,页面标题使用 title-lg。普通表单从 InkForm 的 `layout="col"` 开始;主提交动作明确写 `theme="primary" native-type="submit"`。完整可检查的表单、JSON 保存和适配器示例见 [组合配方](../skills/ui-web/references/composition-recipes.md)。
+普通表单从 InkForm 的 `layout="col"` 开始;主提交动作明确写 `theme="primary" native-type="submit"`。完整可检查的表单、JSON 保存和适配器示例见 [组合配方](../skills/ui-web/references/composition-recipes.md)。
+
+## 按用途选择文字角色
+
+| 内容职责 | 默认角色 |
+| ---------------------------- | --------------------------------- |
+| 普通正文 | body-md;紧凑说明与错误用 body-sm |
+| 控件值、字段标签与短元数据 | label-lg/label-md |
+| 页面、区块与浮层标题 | title-lg/title-sm |
+| 强调正文或引言、醒目页面标题 | body-lg/headline-lg |
+| 代码、标识符 | 相应文字角色,另选等宽字体 |
+
+label-lg 标识控件,body-sm 承载说明。按职责选择角色,不能为了放下说明而改用较小标签。字体家族与尺寸角色分别选择,普通文字默认使用系统 UI 无衬线字体,局部等宽的使用范围见[视觉语言](../docs/design/visual-language.md)。
## CSS、Sass 与 UnoCSS
@@ -65,6 +77,12 @@ font-mono/font-sans 和 underline/no-underline 选择家族及装饰。旧 Uno
## 主题和覆盖边界
+普通表面的文字使用 text.base 或 text.subtle;primary 和 danger 表面分别配 text.on-primary 与 text.on-danger。feedback 下的 error、success、warning、info 用于普通表面上需要强调的反馈前景,不因状态类别而自动应用于所有状态文字;选择时机见[页面组合](../docs/design/composition.md)。这些前景与动作底色分别承担用途,不能自行推导带色容器。图片上的文字应有稳定的承载表面,不能依赖图片恰好够暗。
+
+必要控件边界使用 border.base,辅助分隔使用 border.subtle,键盘焦点使用 border.strong。hover/pressed、焦点、错误与进行状态各有职责;错误边框不能抹掉焦点提示,进行中的动作保留文字与可读配色。配色需要延续主题间的信息主次和状态含义。
+
+用户明确选择的主题优先于系统偏好。调整配色时连同前景、背景和 hover/pressed 等相关状态一起复核;单个色值或派生公式不能独自证明配对可读。
+
`html[data-theme="light"]` 或 `dark` 优先于系统偏好;省略 data-theme 或设为 system 时跟随系统。覆盖样式加载在库之后,主题颜色使用匹配选择器:
```css
@@ -84,6 +102,7 @@ html[data-theme="dark"] {
| ------------------------------------- | ------------------------------------------------------------- |
| 系统颜色、sans/mono 家族、角色四属性 | CSS、apply-font、Uno 角色在运行时读取;颜色须成对复核所有状态 |
| sys.space、sys.radius | CSS/Sass 变量读取与 Uno 间距、圆角工具类共同响应 |
+| apply-elevation | 运行时读取 sys.elevation.raised 对应档位 |
| Sass maps、Uno 尺寸/图标/断点/阴影 | 构建值;改变源后重新生成、构建 |
| 任意 ref 变量 | 不保证带动已经解析的 sys 或 comp,优先覆盖明确的系统角色 |
diff --git a/packages/web/styles/tokens/_comp.scss b/packages/web/styles/tokens/_comp.scss
index 83b2b8e..e644a00 100644
--- a/packages/web/styles/tokens/_comp.scss
+++ b/packages/web/styles/tokens/_comp.scss
@@ -5,17 +5,17 @@
$light: (
"switch": (
- "track-bg": fn.map-deep-get(ref.$color, "neutral", "2"),
+ "track-bg": fn.map-deep-get(ref.$color, "neutral", "12"),
"handle-bg": fn.map-deep-get(ref.$color, "neutral", "98"),
- "label-color": fn.map-deep-get(ref.$color, "neutral", "2")
+ "label-color": fn.map-deep-get(ref.$color, "neutral", "12")
)
);
$dark: (
"switch": (
- "track-bg": fn.map-deep-get(ref.$color, "neutral", "98"),
- "handle-bg": fn.map-deep-get(ref.$color, "neutral", "2"),
- "label-color": fn.map-deep-get(ref.$color, "neutral", "98")
+ "track-bg": fn.map-deep-get(ref.$color, "neutral", "90"),
+ "handle-bg": fn.map-deep-get(ref.$color, "neutral", "12"),
+ "label-color": fn.map-deep-get(ref.$color, "neutral", "90")
)
);
diff --git a/packages/web/styles/tokens/_ref.scss b/packages/web/styles/tokens/_ref.scss
index d4ab9d2..790ff8e 100644
--- a/packages/web/styles/tokens/_ref.scss
+++ b/packages/web/styles/tokens/_ref.scss
@@ -5,25 +5,25 @@ $elevation: (
"low": (
"shadow-type": "dropShadow",
"radius": "2px",
- "color": #0c153014,
+ "color": #0000001a,
"offset-x": "0px",
"offset-y": "1px",
"spread": "0px"
),
"md": (
"shadow-type": "dropShadow",
- "radius": "20px",
- "color": #0c15301f,
+ "radius": "6px",
+ "color": #00000029,
"offset-x": "0px",
- "offset-y": "6px",
+ "offset-y": "2px",
"spread": "0px"
),
"high": (
"shadow-type": "dropShadow",
- "radius": "32px",
- "color": #0c155429,
+ "radius": "12px",
+ "color": #0000002e,
"offset-x": "0px",
- "offset-y": "12px",
+ "offset-y": "4px",
"spread": "0px"
)
)
@@ -54,28 +54,28 @@ $color: (
"10": #410008ff,
"30": #92001eff,
"35": #a80324ff,
- "40": #b9192fff,
+ "40": #b3342bff,
"60": #ff525cff,
"65": #ff7073ff,
- "70": #ff8889ff,
+ "70": #ed9690ff,
"80": #ffb3b1ff,
"90": #ffdad8ff,
"98": #fff8f7ff
),
"brand": (
- "2": #0b1220ff,
- "6": #121928ff,
- "12": #1f2635ff,
- "20": #303747ff,
- "30": #474e5fff,
- "40": #5f6677ff,
- "50": #787e91ff,
- "60": #9198acff,
- "70": #acb3c7ff,
- "80": #b2bccfff,
- "90": #c8cee3ff,
- "94": #f7f7fbff,
- "98": #fbfcffff
+ "2": #070707ff,
+ "6": #131313ff,
+ "12": #1f1f1fff,
+ "20": #303030ff,
+ "30": #474747ff,
+ "40": #5e5e5eff,
+ "50": #777777ff,
+ "60": #919191ff,
+ "70": #abababff,
+ "80": #c6c6c6ff,
+ "90": #e2e2e2ff,
+ "94": #eeeeeeff,
+ "98": #f9f9f9ff
)
);
diff --git a/packages/web/styles/tokens/_sys.scss b/packages/web/styles/tokens/_sys.scss
index ebb7e6f..1f32a16 100644
--- a/packages/web/styles/tokens/_sys.scss
+++ b/packages/web/styles/tokens/_sys.scss
@@ -4,19 +4,19 @@
$color-light: (
"text": (
- "base": fn.map-deep-get(ref.$color, "neutral", "2"),
+ "base": fn.map-deep-get(ref.$color, "neutral", "12"),
"subtle": fn.map-deep-get(ref.$color, "neutral", "40"),
"on-primary": fn.map-deep-get(ref.$color, "brand", "98"),
- "on-danger": fn.map-deep-get(ref.$color, "danger", "98")
+ "on-danger": fn.map-deep-get(ref.$color, "neutral", "98")
),
"surface": (
"base": fn.map-deep-get(ref.$color, "neutral", "100"),
"subtle": fn.map-deep-get(ref.$color, "neutral", "98"),
- "primary": fn.map-deep-get(ref.$color, "brand", "6"),
- "primary-hover": fn.map-deep-get(ref.$color, "brand", "30"),
+ "primary": fn.map-deep-get(ref.$color, "brand", "12"),
+ "primary-hover": fn.map-deep-get(ref.$color, "brand", "20"),
"subtle-hover": fn.map-deep-get(ref.$color, "neutral", "94"),
- "danger": fn.map-deep-get(ref.$color, "danger", "35"),
- "danger-hover": fn.map-deep-get(ref.$color, "danger", "30"),
+ "danger": #825955,
+ "danger-hover": #704947,
"base-hover": fn.map-deep-get(ref.$color, "neutral", "94")
),
"border": (
@@ -26,9 +26,9 @@ $color-light: (
),
"feedback": (
"error": fn.map-deep-get(ref.$color, "danger", "40"),
- "success": #176b39,
- "warning": #7a4b00,
- "info": #005c8a
+ "success": #19713d,
+ "warning": #8a5900,
+ "info": #1267a7
),
"overlay": (
"scrim": rgba(0, 0, 0, 0.5)
@@ -37,31 +37,31 @@ $color-light: (
$color-dark: (
"text": (
- "base": fn.map-deep-get(ref.$color, "neutral", "98"),
- "subtle": fn.map-deep-get(ref.$color, "neutral", "80"),
- "on-primary": fn.map-deep-get(ref.$color, "brand", "2"),
- "on-danger": fn.map-deep-get(ref.$color, "danger", "98")
+ "base": fn.map-deep-get(ref.$color, "neutral", "90"),
+ "subtle": fn.map-deep-get(ref.$color, "neutral", "70"),
+ "on-primary": fn.map-deep-get(ref.$color, "brand", "12"),
+ "on-danger": fn.map-deep-get(ref.$color, "neutral", "12")
),
"surface": (
- "base": fn.map-deep-get(ref.$color, "neutral", "30"),
- "subtle": fn.map-deep-get(ref.$color, "neutral", "12"),
+ "base": fn.map-deep-get(ref.$color, "neutral", "12"),
+ "subtle": fn.map-deep-get(ref.$color, "neutral", "20"),
"primary": fn.map-deep-get(ref.$color, "brand", "90"),
- "primary-hover": fn.map-deep-get(ref.$color, "brand", "60"),
- "subtle-hover": fn.map-deep-get(ref.$color, "neutral", "40"),
- "danger": fn.map-deep-get(ref.$color, "danger", "40"),
- "danger-hover": fn.map-deep-get(ref.$color, "danger", "35"),
- "base-hover": fn.map-deep-get(ref.$color, "neutral", "40")
+ "primary-hover": fn.map-deep-get(ref.$color, "brand", "80"),
+ "subtle-hover": #3a3a3a,
+ "danger": #c8a19d,
+ "danger-hover": #b9918b,
+ "base-hover": fn.map-deep-get(ref.$color, "neutral", "20")
),
"border": (
- "base": fn.map-deep-get(ref.$color, "neutral", "70"),
+ "base": fn.map-deep-get(ref.$color, "neutral", "60"),
"strong": fn.map-deep-get(ref.$color, "neutral", "80"),
"subtle": fn.map-deep-get(ref.$color, "neutral", "40")
),
"feedback": (
- "error": fn.map-deep-get(ref.$color, "danger", "80"),
- "success": #75d69a,
- "warning": #ffd27a,
- "info": #8bceff
+ "error": fn.map-deep-get(ref.$color, "danger", "70"),
+ "success": #67c88c,
+ "warning": #e5b951,
+ "info": #70b5ed
),
"overlay": (
"scrim": rgba(0, 0, 0, 0.5)
diff --git a/packages/web/styles/uno/preset-ink.ts b/packages/web/styles/uno/preset-ink.ts
index c59c84d..2d0f46f 100644
--- a/packages/web/styles/uno/preset-ink.ts
+++ b/packages/web/styles/uno/preset-ink.ts
@@ -177,9 +177,9 @@ export const presetInk = () => ({
"desktop": "1280px"
},
boxShadow: {
- "low": "0px 1px 2px 0px #0c153014",
- "md": "0px 6px 20px 0px #0c15301f",
- "high": "0px 12px 32px 0px #0c155429"
+ "low": "0px 1px 2px 0px #0000001a",
+ "md": "0px 2px 6px 0px #00000029",
+ "high": "0px 4px 12px 0px #0000002e"
},
},
rules: [
diff --git a/scripts/build-agent-skills.md b/scripts/build-agent-skills.md
index 01eacf5..7fd6814 100644
--- a/scripts/build-agent-skills.md
+++ b/scripts/build-agent-skills.md
@@ -1,10 +1,10 @@
# UI Web Agent 文档生成
-`build-agent-skills.ts` 生成 `packages/web/skills/ui-web/`,并把根级 DESIGN.md 同步到包内。两份 DESIGN.md 内容一致,只有仓库根级源允许人工编辑。
+`build-agent-skills.ts` 生成 `packages/web/skills/ui-web/`,并把根级 DESIGN.md 与 docs/design 正文和图片按原路径同步到包内。对应文件字节一致,只有仓库根级源允许人工编辑。
## 内容责任
-DESIGN.md 拥有共同设计判断;`skill.seed.json` 拥有组件意图、选型、组合步骤和集成注意事项。组件源码与公开类型拥有 API 事实,Story 拥有可运行示例与展示状态。生成器组合这些来源,生成目录不保留手工文件。
+DESIGN.md 拥有总纲与按任务阅读的路径;docs/design 分别拥有设计立场、视觉语言、页面组合和判断依据;`skill.seed.json` 拥有组件意图、选型、组合步骤和集成注意事项。组件源码与公开类型拥有 API 事实,Story 拥有可运行示例与展示状态。生成器组合这些来源,生成目录不保留手工文件。
API 通过 Vue 官方 `vue-component-meta` 读取 props、models 对应的更新事件、事件载荷和插槽参数,枚举类型展开为可读选择。工具版本与 vue-tsc 一致。Vue 元数据不能识别本库 prop helper 的默认参数,因此 `lib/component-api.ts` 用 TypeScript AST 补读这些 helper 调用;不执行组件或默认工厂,未知 helper 或无法定位的声明明确失败。更改 helper 语义时须同步这段适配并复核生成结果。
@@ -19,9 +19,9 @@ pnpm build-skills
pnpm check:skills
```
-前者写入生成物与设计指南副本;后者检查过期内容并验证 Intent 结构。包构建中的 `build:skills` 也执行过期检查。总检查中的 `check:generated` 只重建 Token 和包元数据,Skill/DESIGN.md 的一致性由包构建检查,Intent 结构单独检查,避免重复生成同一批文档。
+前者写入生成物与整组设计文档副本;后者检查文件集合与内容是否过期,并验证 Intent 结构。包构建中的 `build:skills` 也执行过期检查。总检查中的 `check:generated` 只重建 Token 和包元数据,Skill/设计文档的一致性由包构建检查,Intent 结构单独检查,避免重复生成同一批文档。
-`check-package-contract.ts` 从 tarball 在临时目录独立安装,读取同版本 DESIGN.md 和 Skill,检查包内链接与 Intent 发现。允许包内相对跳转,不允许逃出安装包去读取生产者源码或任务记录。
+`check-package-contract.ts` 从 tarball 在临时目录独立安装,读取同版本设计总纲、正文、实例图片与 Skill,检查包内链接与 Intent 发现。允许包内相对跳转,不允许逃出安装包去读取生产者源码或任务记录。
安装与发现方式见 [Web 包说明](../packages/web/README.md#agent-skill)。消费者显式配置可信包;也可以直接读取 Markdown。组织工作流 Skill 和仓库维护授权不随产品包分发。
diff --git a/scripts/build-agent-skills.ts b/scripts/build-agent-skills.ts
index 72f9777..66eb810 100644
--- a/scripts/build-agent-skills.ts
+++ b/scripts/build-agent-skills.ts
@@ -1,4 +1,12 @@
-import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import {
+ copyFileSync,
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { createChecker, type ComponentMeta } from "vue-component-meta";
import { propDefault, propType, typeDeclarations } from "./lib/component-api.js";
@@ -6,6 +14,7 @@ import {
repositoryRoot,
readComponentManifest,
readPackageManifest,
+ listDesignFiles,
resolveWebPackageRoot,
type PublicComponent,
} from "./lib/ui-package.js";
@@ -247,7 +256,7 @@ matches a product task.
## Workflow
1. Identify the product intent and interaction state.
-2. Read [DESIGN.md](../../DESIGN.md) for design decisions, defaults, and extension boundaries; then read \`references/component-map.md\` to shortlist components.
+2. 从 [DESIGN.md](../../DESIGN.md) 的总纲按当前任务读取设计正文;无需加载全部文件,再用 \`references/component-map.md\` 选择组件。
3. For multi-component work, read \`references/composition-recipes.md\`.
4. Load only the selected files under \`references/components/\`.
5. Check integration, styling, and common-mistake references only when relevant.
@@ -280,7 +289,7 @@ ${bullets(model.skill.rules)}
## References
-- [DESIGN.md](../../DESIGN.md): shared design decisions for consumers and maintainers.
+- [DESIGN.md](../../DESIGN.md): 设计总纲与按任务阅读的路径;正文分别说明立场、视觉、组合和判断依据。
- \`references/component-map.md\`: intent-to-component routing.
- \`references/composition-recipes.md\`: reviewed multi-component workflows.
- \`references/integration.md\`: installation, router, and i18n boundaries.
@@ -572,13 +581,30 @@ function writeOutputs(outputs: Map): void {
try {
const model = buildModel();
const outputs = buildOutputs(model);
- const designSource = readText(resolve(repositoryRoot, "DESIGN.md"));
- const designTarget = resolve(packageRoot, "DESIGN.md");
+ const designFiles = listDesignFiles();
if (checkMode) {
- if (readTextIfPresent(designTarget) !== designSource)
- throw new Error("Packaged DESIGN.md is stale");
+ const packagedFiles = existsSync(resolve(packageRoot, "docs/design"))
+ ? listDesignFiles(packageRoot)
+ : ["DESIGN.md"];
+ if (JSON.stringify(packagedFiles) !== JSON.stringify(designFiles)) {
+ throw new Error("Packaged design document file set is stale");
+ }
+ for (const file of designFiles) {
+ const target = resolve(packageRoot, file);
+ if (
+ !existsSync(target) ||
+ !readFileSync(target).equals(readFileSync(resolve(repositoryRoot, file)))
+ ) {
+ throw new Error(`Packaged design document is stale: ${file}`);
+ }
+ }
} else {
- writeFileSync(designTarget, designSource, "utf8");
+ rmSync(resolve(packageRoot, "docs/design"), { recursive: true, force: true });
+ for (const file of designFiles) {
+ const target = resolve(packageRoot, file);
+ mkdirSync(dirname(target), { recursive: true });
+ copyFileSync(resolve(repositoryRoot, file), target);
+ }
}
if (checkMode) {
diff --git a/scripts/check-package-contract.ts b/scripts/check-package-contract.ts
index b670b7c..4af49ca 100644
--- a/scripts/check-package-contract.ts
+++ b/scripts/check-package-contract.ts
@@ -17,6 +17,7 @@ import * as sass from "sass";
import {
readComponentManifest,
readPackageManifest,
+ listDesignFiles,
repositoryRoot,
resolveWebPackageRoot,
} from "./lib/ui-package.js";
@@ -35,6 +36,7 @@ interface PublishedPackageManifest {
const packageRoot = resolveWebPackageRoot();
const packageManifest = readPackageManifest(packageRoot);
const publicComponents = readComponentManifest(packageRoot);
+const designFiles = listDesignFiles();
const targetPackageName = packageManifest.name;
const temporaryRoot = mkdtempSync(join(tmpdir(), "inkcre-ui-contract-"));
@@ -239,7 +241,7 @@ try {
}
const requiredFiles = [
- "DESIGN.md",
+ ...designFiles,
"dist/index.js",
"dist/index.d.ts",
"dist/components.d.ts",
@@ -311,7 +313,12 @@ try {
dialogReference.includes("isLoading: boolean"),
"Scoped slot arguments must be available to consumers",
);
- for (const file of ["README.md", "DESIGN.md", "MIGRATION.md", "styles/README.md"]) {
+ for (const file of [
+ "README.md",
+ "MIGRATION.md",
+ "styles/README.md",
+ ...designFiles.filter((file) => file.endsWith(".md")),
+ ]) {
const content = readFileSync(resolve(packedRoot, file), "utf8");
for (const match of content.matchAll(/\]\(([^)]+)\)/g)) {
if (/^(?:https?:|#)/.test(match[1])) continue;
@@ -336,10 +343,13 @@ try {
throw new Error(`Packed CSS references undefined design tokens: ${missingTokens.join(", ")}`);
}
- assert.equal(
- readFileSync(resolve(packedRoot, "DESIGN.md"), "utf8"),
- readFileSync(resolve(repositoryRoot, "DESIGN.md"), "utf8"),
- );
+ assert.deepEqual(listDesignFiles(packedRoot), designFiles);
+ for (const file of designFiles) {
+ assert.ok(
+ readFileSync(resolve(packedRoot, file)).equals(readFileSync(resolve(repositoryRoot, file))),
+ `Packed design document differs from its source: ${file}`,
+ );
+ }
assert.ok(
!existsSync(resolve(consumerModules, "unocss")),
"Basic install must not require UnoCSS",
diff --git a/scripts/lib/ui-package.ts b/scripts/lib/ui-package.ts
index 65675aa..e427293 100644
--- a/scripts/lib/ui-package.ts
+++ b/scripts/lib/ui-package.ts
@@ -1,5 +1,5 @@
-import { existsSync, readFileSync } from "node:fs";
-import { dirname, resolve } from "node:path";
+import { existsSync, readFileSync, readdirSync } from "node:fs";
+import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
export interface PublicComponent {
@@ -64,3 +64,13 @@ export function readComponentManifest(packageRoot = resolveWebPackageRoot()): Pu
return [...manifest.components].sort((left, right) => left.name.localeCompare(right.name));
}
+
+/** The design source and its package copy preserve the same relative links. */
+export function listDesignFiles(root = repositoryRoot): string[] {
+ return [
+ "DESIGN.md",
+ ...readdirSync(resolve(root, "docs/design"), { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile())
+ .map((entry) => relative(root, resolve(entry.parentPath, entry.name)).split("\\").join("/")),
+ ].sort();
+}
diff --git a/scripts/prepare-token-update.ts b/scripts/prepare-token-update.ts
index d5c56b9..4d3904d 100644
--- a/scripts/prepare-token-update.ts
+++ b/scripts/prepare-token-update.ts
@@ -254,6 +254,8 @@ function runFixture(): void {
assert.equal(existsSync(resolve(temporaryRoot, ".changeset/figma-invalid-1.md")), false);
}
const cycle = structuredClone(source);
+ // 构造闭环的两端,避免依赖当前正文恰好引用哪一级中性色。
+ cycle.sys.light.color.text.base.value = "{ref.color.neutral.2}";
cycle.ref.color.neutral["2"].value = "{sys.light.color.text.base}";
const cycleRoot = resolve(temporaryRoot, "cycle");
mkdirSync(resolve(cycleRoot, "tokens"), { recursive: true });
diff --git a/tasks/ui-foundations/color-study.html b/tasks/ui-foundations/color-study.html
new file mode 100644
index 0000000..5c4ff9c
--- /dev/null
+++ b/tasks/ui-foundations/color-study.html
@@ -0,0 +1,92 @@
+
+
+
+
+InKCre · 色调对照
+
+
+InKCre · 色调对照
+这次只比较黑白灰底色的冷暖。三组使用相同的排版、布局和灰调彩色,便于看清底色如何改变整体感受。色值仅用于本次讨论。
+
+
+
+
+
+
+
+
+
+
diff --git a/tasks/ui-foundations/color-study.png b/tasks/ui-foundations/color-study.png
new file mode 100644
index 0000000..c7b5b19
Binary files /dev/null and b/tasks/ui-foundations/color-study.png differ
diff --git a/tasks/ui-foundations/header-evidence/README.md b/tasks/ui-foundations/header-evidence/README.md
new file mode 100644
index 0000000..3505707
--- /dev/null
+++ b/tasks/ui-foundations/header-evidence/README.md
@@ -0,0 +1,17 @@
+# Header 菜单图标修复验收
+
+原按钮直接承载 Uno mask,又把填色重置为透明。修复将 mask 放到 aria-hidden 的子 span,保留原生按钮、Menu 名称、至少 24×24px 操作区域与 menu-click。按钮自身没有 mask,键盘焦点轮廓不会随图标裁剪。
+
+完整 `pnpm check` 与正式 Story 构建的本地浏览器检查通过,见 [本地结果](local.json)。浅深主题分别在 375px/1280px 验证 24px 图形尺寸、图标 mask、非透明且跟随文字的填色、按钮透明背景与未裁剪的焦点,以及点击图标、Enter、Space 每次只发出一个菜单事件。pageerror 为零。
+
+[浅色菜单及焦点](header-light-375.png)、[深色菜单及焦点](header-dark-375.png)已人工复核。Basic Story 用输出次数呈现事件;它是组件交互示例,不是业务页面布局范本。
+
+复跑:
+
+```sh
+pnpm exec node tasks/ui-foundations/header-evidence/verify.mjs /absolute/path/to/@playwright/test/index.mjs https://pr-46.design-dd4.pages.dev
+```
+
+脚本复用既有 Playwright,不新增组件套件。必须对包含本修复的产物执行;registry UI 2.0.0 及当前消费者 PR #104 仍含原缺陷,不能将本地探针或生产者 preview 视为下游已修复。消费者需安装正式修复版本后复验。
+
+最终源码 `d6125cf` 的 [CI](https://github.com/InKCre/ui/actions/runs/34766090160) 与 [preview 交付](https://github.com/InKCre/ui/actions/runs/34766172549) 成功。上述四组检查在实际远端重复通过,24px 图形、填色、焦点和菜单事件正常,pageerror 为零;见 [远端结果与部署身份](verification.json)。截图已更新为该部署的实际结果并人工复核。
diff --git a/tasks/ui-foundations/header-evidence/header-dark-1280.png b/tasks/ui-foundations/header-evidence/header-dark-1280.png
new file mode 100644
index 0000000..de16da4
Binary files /dev/null and b/tasks/ui-foundations/header-evidence/header-dark-1280.png differ
diff --git a/tasks/ui-foundations/header-evidence/header-dark-375.png b/tasks/ui-foundations/header-evidence/header-dark-375.png
new file mode 100644
index 0000000..54b13db
Binary files /dev/null and b/tasks/ui-foundations/header-evidence/header-dark-375.png differ
diff --git a/tasks/ui-foundations/header-evidence/header-light-1280.png b/tasks/ui-foundations/header-evidence/header-light-1280.png
new file mode 100644
index 0000000..1de9d37
Binary files /dev/null and b/tasks/ui-foundations/header-evidence/header-light-1280.png differ
diff --git a/tasks/ui-foundations/header-evidence/header-light-375.png b/tasks/ui-foundations/header-evidence/header-light-375.png
new file mode 100644
index 0000000..ab0a7dd
Binary files /dev/null and b/tasks/ui-foundations/header-evidence/header-light-375.png differ
diff --git a/tasks/ui-foundations/header-evidence/local.json b/tasks/ui-foundations/header-evidence/local.json
new file mode 100644
index 0000000..4c88b07
--- /dev/null
+++ b/tasks/ui-foundations/header-evidence/local.json
@@ -0,0 +1,59 @@
+{
+ "origin": "http://127.0.0.1:5210",
+ "browser": "149.0.7827.55",
+ "checks": [
+ {
+ "theme": "light",
+ "width": 375,
+ "foreground": "rgb(31, 31, 31)",
+ "bounds": {
+ "x": 319,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "light",
+ "width": 1280,
+ "foreground": "rgb(31, 31, 31)",
+ "bounds": {
+ "x": 1224,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "dark",
+ "width": 375,
+ "foreground": "rgb(226, 226, 226)",
+ "bounds": {
+ "x": 319,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "dark",
+ "width": 1280,
+ "foreground": "rgb(226, 226, 226)",
+ "bounds": {
+ "x": 1224,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ }
+ ],
+ "errors": []
+}
diff --git a/tasks/ui-foundations/header-evidence/verification.json b/tasks/ui-foundations/header-evidence/verification.json
new file mode 100644
index 0000000..9654878
--- /dev/null
+++ b/tasks/ui-foundations/header-evidence/verification.json
@@ -0,0 +1,63 @@
+{
+ "origin": "https://pr-46.design-dd4.pages.dev",
+ "browser": "149.0.7827.55",
+ "checks": [
+ {
+ "theme": "light",
+ "width": 375,
+ "foreground": "rgb(31, 31, 31)",
+ "bounds": {
+ "x": 319,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "light",
+ "width": 1280,
+ "foreground": "rgb(31, 31, 31)",
+ "bounds": {
+ "x": 1224,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "dark",
+ "width": 375,
+ "foreground": "rgb(226, 226, 226)",
+ "bounds": {
+ "x": 319,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ },
+ {
+ "theme": "dark",
+ "width": 1280,
+ "foreground": "rgb(226, 226, 226)",
+ "bounds": {
+ "x": 1224,
+ "y": 18,
+ "width": 24,
+ "height": 24
+ },
+ "menuEvents": 3,
+ "mask": "visible child glyph; unmasked button and focus outline"
+ }
+ ],
+ "errors": [],
+ "source": "d6125cf837e5f79f85801c9136ae805625d3c427",
+ "deployment": "https://38e5661b.design-dd4.pages.dev",
+ "ci": "https://github.com/InKCre/ui/actions/runs/34766090160",
+ "preview_run": "https://github.com/InKCre/ui/actions/runs/34766172549"
+}
diff --git a/tasks/ui-foundations/header-evidence/verify.mjs b/tasks/ui-foundations/header-evidence/verify.mjs
new file mode 100644
index 0000000..2eccccb
--- /dev/null
+++ b/tasks/ui-foundations/header-evidence/verify.mjs
@@ -0,0 +1,69 @@
+// 本任务的 Header 回归验收,复用现有 Playwright 与正式 Story 构建。
+import { pathToFileURL } from "node:url";
+import { writeFile } from "node:fs/promises";
+const { chromium, expect } = await import(pathToFileURL(process.argv[2]).href);
+const origin = process.argv[3];
+const browser = await chromium.launch();
+const page = await browser.newPage();
+const errors = [];
+page.on("pageerror", (error) => errors.push(error.message));
+const report = { origin, browser: browser.version(), checks: [], errors };
+try {
+ for (const theme of ["light", "dark"]) {
+ for (const width of [375, 1280]) {
+ await page.setViewportSize({ width, height: 800 });
+ await page.goto(
+ `${origin}/__sandbox.html?storyId=stories-controls-inkheader-story-vue&variantId=stories-controls-inkheader-story-vue-0`,
+ );
+ const menu = page.getByRole("button", { name: "Menu", exact: true });
+ await menu.waitFor();
+ await page.evaluate(
+ (theme) => document.documentElement.classList.toggle("dark", theme === "dark"),
+ theme,
+ );
+ await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
+ const glyph = menu.locator('[aria-hidden="true"]');
+ await expect(glyph).toBeVisible();
+ await expect(glyph).not.toHaveCSS("mask-image", "none");
+ await expect(glyph).toHaveCSS("width", "24px");
+ await expect(glyph).toHaveCSS("height", "24px");
+ const foreground = await menu.evaluate((e) => getComputedStyle(e).color);
+ await expect(glyph).toHaveCSS("background-color", foreground);
+ await expect(glyph).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+ await expect(menu).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+ await expect(menu).toHaveCSS("mask-image", "none");
+ const bounds = await menu.boundingBox();
+ expect(bounds.width).toBeGreaterThanOrEqual(24);
+ expect(bounds.height).toBeGreaterThanOrEqual(24);
+ expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
+ await glyph.click();
+ await expect(page.locator("output")).toHaveText("菜单点击次数:1");
+ await menu.press("Enter");
+ await expect(page.locator("output")).toHaveText("菜单点击次数:2");
+ await menu.press("Space");
+ await expect(page.locator("output")).toHaveText("菜单点击次数:3");
+ await expect(menu).toBeFocused();
+ await expect(menu).toHaveCSS("outline-style", "solid");
+ await expect(menu).toHaveCSS("outline-width", "2px");
+ await page
+ .locator(".ink-header")
+ .screenshot({ path: new URL(`header-${theme}-${width}.png`, import.meta.url).pathname });
+ report.checks.push({
+ theme,
+ width,
+ foreground,
+ bounds,
+ menuEvents: 3,
+ mask: "visible child glyph; unmasked button and focus outline",
+ });
+ }
+ }
+ expect(errors).toEqual([]);
+ await writeFile(
+ new URL("verification.json", import.meta.url),
+ JSON.stringify(report, null, 2) + "\n",
+ );
+ console.log(JSON.stringify(report));
+} finally {
+ await browser.close();
+}
diff --git a/tasks/ui-foundations/i1-evidence/README.md b/tasks/ui-foundations/i1-evidence/README.md
new file mode 100644
index 0000000..c864c73
--- /dev/null
+++ b/tasks/ui-foundations/i1-evidence/README.md
@@ -0,0 +1,32 @@
+# I1 字体、轮廓与示例验收
+
+2026-09-13,在 `feat/design-knowledge` 的未提交修改上完成。生产者包仍标记为 2.0.0,以下为本地候选产物,不是 registry 上已发布的 2.0.0。消费者继续使用正式版本,其局部修改由独立迁移 worktree 的工作包记录。
+
+## 产物与检查
+
+最终 `pnpm check` 通过,覆盖格式、lint、生成一致性、Intent、Token 工作流、类型、构建、两种包身份的严格独立安装及文档链接,以及 21 个 Story/120 个 Variant 的构建。首轮 Story 构建发现新样式缺少 Sass helper 的显式导入;补入后重新运行完整检查通过,没有扩大自动注入范围。
+
+随后用 `pnpm --filter @inkcre/ui-web pack --pack-destination "$PWD/tmp/i1-pack"` 打包,SHA-256 为 `7be8c129cdee364977199b592da15f37cfca2af3df30d9c408f0afa58967ca8b`。临时 Vue/Vite 页面直接导入解包后的 `dist/index.js` 与 `dist/index.css`,没有引用组件源码。该检查补充已有的严格安装检查,不将复用仓库浏览器工具的页面称为独立安装。
+
+## 浏览器结果
+
+使用既有下游 Playwright 的 Chromium 149.0.7827.55;浏览器与临时服务器在验收后关闭,没有增加测试套件、依赖或发布入口。
+
+| 边界 | 实际结果 |
+| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
+| Switch 普通文案 | 外层使用 mono 时,控件仍采用系统 UI sans;技术性内容可以继续在各自局部使用 mono |
+| Switch 状态和尺寸 | 中英文、xs/sm/md/lg、浅深主题、320px/768px 下,Space/Enter 切换和 pending 前后的八个轨道宽度一致;pending 禁止操作 |
+| AutoForm 根错误 | 非对象 schema 的错误直接可见,直角容器;320px 可完整换行,无页面横向溢出 |
+| Image 缩略图槽 | 查看提示持续可见;Enter 打开、Escape 关闭并恢复缩略图焦点,浅深主题均通过 |
+| Image 预览槽 | 自定义标题有清晰的前景/背景配对;下载链接实际保存 `inkcre.svg`,文件包含 SVG 内容,下载后预览保持打开 |
+| AutoForm Story 调试内容 | JSON 使用 body-sm mono,浅深背景与文字同步变化,容器不再有硬编码圆角 |
+
+数值记录见 [打包组件结果](report.json) 与 [Story 操作结果](story-report.json)。代表图片为 [320px 浅色](components-light-320.png) 和 [320px 深色](components-dark-320.png);它们是组件状态阵列,不是完整页面视觉样板,也不表示现有反馈色或阴影数值已完成校准。
+
+Story 的复核入口为根级 `pnpm story:dev`。在 Controls/Switch 查看“长状态文案保持轨道尺寸”,在 Forms/AutoForm 查看“Invalid Schema Handling”和表单数据,在 Media/Image 操作“Custom Thumbnail Slot”“With Expanded Footer”“With Custom Header”。这批示例保持现有 Variant 数量,没有另造一套组件演示。
+
+## 下游边界
+
+独立 `feat/ui-v2-migration` worktree 的完整 `pnpm check` 与真实 Host/MF remote 重放通过。Twitter 步骤编号采用直角;375px 截图发现的向导宽度溢出同时修正,关闭按钮完整可见。已有中文注入、时间取消/确认、Mail 下载 pending/失败及窄宽页面旅程继续通过,pageerror 为零。
+
+消费者仍锁定 registry UI 2.0.0,因此上述下游结果不代表生产者的新字体或设计文档已发布、安装。颜色/阴影数值的实验和页面内容层级复审仍分别属于 I2、I3。
diff --git a/tasks/ui-foundations/i1-evidence/components-dark-320.png b/tasks/ui-foundations/i1-evidence/components-dark-320.png
new file mode 100644
index 0000000..24f8292
Binary files /dev/null and b/tasks/ui-foundations/i1-evidence/components-dark-320.png differ
diff --git a/tasks/ui-foundations/i1-evidence/components-light-320.png b/tasks/ui-foundations/i1-evidence/components-light-320.png
new file mode 100644
index 0000000..19d6db8
Binary files /dev/null and b/tasks/ui-foundations/i1-evidence/components-light-320.png differ
diff --git a/tasks/ui-foundations/i1-evidence/report.json b/tasks/ui-foundations/i1-evidence/report.json
new file mode 100644
index 0000000..3472748
--- /dev/null
+++ b/tasks/ui-foundations/i1-evidence/report.json
@@ -0,0 +1,54 @@
+{
+ "browser": "149.0.7827.55",
+ "cases": [
+ {
+ "theme": "light",
+ "width": 320,
+ "widths": [116.4375, 90, 116.4375, 90, 116.4375, 100, 120, 120],
+ "font": {
+ "control": "system-ui, -apple-system, sans-serif",
+ "body": "system-ui, -apple-system, sans-serif",
+ "host": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace"
+ },
+ "error": "Expected an object schema with flat properties",
+ "overflow": 0
+ },
+ {
+ "theme": "light",
+ "width": 768,
+ "widths": [116.4375, 90, 116.4375, 90, 116.4375, 100, 120, 120],
+ "font": {
+ "control": "system-ui, -apple-system, sans-serif",
+ "body": "system-ui, -apple-system, sans-serif",
+ "host": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace"
+ },
+ "error": "Expected an object schema with flat properties",
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "width": 320,
+ "widths": [116.4375, 90, 116.4375, 90, 116.4375, 100, 120, 120],
+ "font": {
+ "control": "system-ui, -apple-system, sans-serif",
+ "body": "system-ui, -apple-system, sans-serif",
+ "host": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace"
+ },
+ "error": "Expected an object schema with flat properties",
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "width": 768,
+ "widths": [116.4375, 90, 116.4375, 90, 116.4375, 100, 120, 120],
+ "font": {
+ "control": "system-ui, -apple-system, sans-serif",
+ "body": "system-ui, -apple-system, sans-serif",
+ "host": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace"
+ },
+ "error": "Expected an object schema with flat properties",
+ "overflow": 0
+ }
+ ],
+ "errors": []
+}
diff --git a/tasks/ui-foundations/i1-evidence/story-report.json b/tasks/ui-foundations/i1-evidence/story-report.json
new file mode 100644
index 0000000..b0dec59
--- /dev/null
+++ b/tasks/ui-foundations/i1-evidence/story-report.json
@@ -0,0 +1,43 @@
+{
+ "checks": [
+ {
+ "theme": "light",
+ "customThumbnail": "keyboard open, Escape close, focus return",
+ "download": "inkcre.svg, valid SVG, dialog remains open",
+ "header": {
+ "color": "rgb(7, 7, 7)",
+ "background": "rgb(255, 255, 255)",
+ "font": "system-ui, -apple-system, sans-serif"
+ }
+ },
+ {
+ "theme": "dark",
+ "customThumbnail": "keyboard open, Escape close, focus return",
+ "download": "inkcre.svg, valid SVG, dialog remains open",
+ "header": {
+ "color": "rgb(249, 249, 249)",
+ "background": "rgb(71, 71, 71)",
+ "font": "system-ui, -apple-system, sans-serif"
+ }
+ },
+ {
+ "theme": "light",
+ "autoFormDebug": {
+ "radius": "0px",
+ "font": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace",
+ "color": "rgb(7, 7, 7)",
+ "background": "rgb(249, 249, 249)"
+ }
+ },
+ {
+ "theme": "dark",
+ "autoFormDebug": {
+ "radius": "0px",
+ "font": "\"SF Mono\", Monaco, \"Roboto Mono\", monospace",
+ "color": "rgb(249, 249, 249)",
+ "background": "rgb(31, 31, 31)"
+ }
+ }
+ ],
+ "errors": []
+}
diff --git a/tasks/ui-foundations/i2-evidence/README.md b/tasks/ui-foundations/i2-evidence/README.md
new file mode 100644
index 0000000..e1244d9
--- /dev/null
+++ b/tasks/ui-foundations/i2-evidence/README.md
@@ -0,0 +1,38 @@
+# I2 正式源与产物验收
+
+用户已认可[修订后的场景校准](../i2-study/README.md)。选定的配色、Switch 与阴影值现已进入 `tokens/inkcre.tokens.json`,沿既有生成链同步 Sass、CSS 和 Uno 输出。150 个公开 Token 路径及类型保持不变,没有新增主题机制、角色或依赖。
+
+`ref.color.brand` 引用对应中性色阶;红色基础值中更新了反馈使用的 40/70 两级。危险动作底色直接由 surface.danger/danger-hover 维护,与较纯的错误前景分开,未新增另一套色阶。反馈角色说明、设计与样式指南、Agent 配方和普通提交示例同步更新;迁移指南说明新默认表现,并将已发布的 2.0.0 要求与当前未发布改动分开。
+
+## 验证结果
+
+完整 `pnpm check` 通过,涵盖格式、lint、生成一致性、Token 导入边界、类型、独立 tarball 安装与公开入口、文档/配方交付,以及 21 个 Story/120 个 Variant 构建。循环引用夹具原先隐式依赖正文指向 neutral.2,本轮改为显式构造闭环的两端;生成器与导入策略没有改变。
+
+[实际产物验收页](index.html)直接使用本地 tarball 的组件和根级主题,不覆盖候选色值。[浏览器记录](verification.json)由 Chromium 149.0.7827.55 生成:
+
+- 浅深主题各 41 个颜色、Switch 与阴影变量和冻结样稿一致。此前候选 84 个配对的结果因而适用于该产物:浅/深最低文字对比约 5.14:1/4.95:1,必要边界约 3.86:1/3.61:1。
+- primary/subtle/danger 的普通、hover、pressed、焦点、disabled 与 pending 配对通过。错误输入保留独立焦点提示;Dropdown 的选中标签、说明及相邻活动项分别使用对应前景。
+- Popup/Scrim 实际 Teleport 到 body,表面、文字与遮罩使用根级主题;Popup 打开期间切换根级主题也能更新。375px 下确认操作完整可见,关闭后焦点返回触发按钮,页面无横向溢出。
+- Sass 与 Uno 三档可见阴影和样稿一致,实际 Tooltip 使用相同的默认档。运行时修改 md 模糊变量会更新 Sass 与 Tooltip,Uno 保持构建值;其工具类的前景/背景配对也通过验证。
+- 页面脚本错误与 HTTP(S) 请求均为零。已查看六张截图,浏览器已关闭;离线页面无需服务。
+
+截图:控件与选中项[浅色](controls-light.png)/[深色](controls-dark.png),375px Popup [浅色](popup-light-375.png)/[深色](popup-dark-375.png),375px Scrim [浅色](scrim-light-375.png)/[深色](scrim-dark-375.png)。这是状态与公开入口的验收夹具,阴影探针及按钮矩阵不作为真实页面的组合范本。
+
+Uno 验收保留 presetWind3 的初始化样式;阴影工具类依赖它提供的 ring/shadow 变量。记录保留完整计算值,比较时排除透明的 ring 合成层。UI 包仍不引入 preflight,夹具没有向生产包添加全局样式。
+
+## 产物身份与复跑
+
+本地 tarball 仍标记 `@inkcre/ui-web@2.0.0`,**不是 registry 已发布的 2.0.0**。SHA-256 为 `a2d8e5ecffeaa58aca0ea60e4bcfdf77d486d9b694c91fbc3775ec5708da0f5d`。基线提交为 `11be433`,叠加尚未提交的 I2 源;[artifact.json](artifact.json)另记录 Token 源与 CSS 的哈希,source 字段不表示 I2 已提交或发布。
+
+从仓库根执行:
+
+```sh
+pnpm check
+pnpm --dir packages/web pack --pack-destination ../../tmp/i2-pack
+node tasks/ui-foundations/i2-study/build.mjs tmp/i2-pack/inkcre-ui-web-2.0.0.tgz --delivery
+node tasks/ui-foundations/i2-study/verify-delivery.mjs /path/to/existing/playwright/index.mjs
+```
+
+构建与浏览器脚本复用现有工具,不建立组件测试套件。完整检查日志为本机 `/tmp/inkcre-i2-check.log`,浏览器日志为 `/tmp/inkcre-i2-delivery-verify.log`。构建会清理临时解包目录,实验与证据随父工作包撤除。
+
+生产者 I2 已验证、未提交或发布。消费者 worktree 保持干净,仍使用 registry UI 2.0.0;后续发布后需要按真实安装版本复验,不能把此处的产物夹具当作下游迁移完成。I3 继续拥有真实页面的内容取舍与状态组织。
diff --git a/tasks/ui-foundations/i2-evidence/artifact.json b/tasks/ui-foundations/i2-evidence/artifact.json
new file mode 100644
index 0000000..f7f8a0e
--- /dev/null
+++ b/tasks/ui-foundations/i2-evidence/artifact.json
@@ -0,0 +1,7 @@
+{
+ "source": "11be433ca6e7c203da7f42233983e576a967d71e",
+ "package": "@inkcre/ui-web@2.0.0",
+ "tarballSha256": "a2d8e5ecffeaa58aca0ea60e4bcfdf77d486d9b694c91fbc3775ec5708da0f5d",
+ "tokensSha256": "475712a34e15dc4e474b2b0308110f5423cdf92e66a01bf5de542771c342e593",
+ "cssSha256": "aa16dd5b8a35dbd20ec70e335aab9a1afb473d8a50417fb77e59140f48125b7b"
+}
diff --git a/tasks/ui-foundations/i2-evidence/controls-dark.png b/tasks/ui-foundations/i2-evidence/controls-dark.png
new file mode 100644
index 0000000..8165683
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/controls-dark.png differ
diff --git a/tasks/ui-foundations/i2-evidence/controls-light.png b/tasks/ui-foundations/i2-evidence/controls-light.png
new file mode 100644
index 0000000..16ee886
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/controls-light.png differ
diff --git a/tasks/ui-foundations/i2-evidence/index.html b/tasks/ui-foundations/i2-evidence/index.html
new file mode 100644
index 0000000..e4cebf7
--- /dev/null
+++ b/tasks/ui-foundations/i2-evidence/index.html
@@ -0,0 +1,30 @@
+InKCre · I2 交付验收
diff --git a/tasks/ui-foundations/i2-evidence/popup-dark-375.png b/tasks/ui-foundations/i2-evidence/popup-dark-375.png
new file mode 100644
index 0000000..401b466
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/popup-dark-375.png differ
diff --git a/tasks/ui-foundations/i2-evidence/popup-light-375.png b/tasks/ui-foundations/i2-evidence/popup-light-375.png
new file mode 100644
index 0000000..83b9ee7
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/popup-light-375.png differ
diff --git a/tasks/ui-foundations/i2-evidence/scrim-dark-375.png b/tasks/ui-foundations/i2-evidence/scrim-dark-375.png
new file mode 100644
index 0000000..f8edc50
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/scrim-dark-375.png differ
diff --git a/tasks/ui-foundations/i2-evidence/scrim-light-375.png b/tasks/ui-foundations/i2-evidence/scrim-light-375.png
new file mode 100644
index 0000000..332a4bb
Binary files /dev/null and b/tasks/ui-foundations/i2-evidence/scrim-light-375.png differ
diff --git a/tasks/ui-foundations/i2-evidence/verification.json b/tasks/ui-foundations/i2-evidence/verification.json
new file mode 100644
index 0000000..027fd26
--- /dev/null
+++ b/tasks/ui-foundations/i2-evidence/verification.json
@@ -0,0 +1,159 @@
+{
+ "source": "11be433ca6e7c203da7f42233983e576a967d71e",
+ "package": "@inkcre/ui-web@2.0.0",
+ "tarballSha256": "a2d8e5ecffeaa58aca0ea60e4bcfdf77d486d9b694c91fbc3775ec5708da0f5d",
+ "tokensSha256": "475712a34e15dc4e474b2b0308110f5423cdf92e66a01bf5de542771c342e593",
+ "cssSha256": "aa16dd5b8a35dbd20ec70e335aab9a1afb473d8a50417fb77e59140f48125b7b",
+ "approvedStudySha256": "2f6dba098c22217d001b6905fd87df1dbdaa2fcf00968c1dab7fed07266b8e2e",
+ "browser": "149.0.7827.55",
+ "themes": [
+ {
+ "theme": "light",
+ "matchedVariables": 41,
+ "tokens": {
+ "--comp-switch-handle-bg": "rgb(249, 249, 249)",
+ "--comp-switch-label-color": "rgb(31, 31, 31)",
+ "--comp-switch-track-bg": "rgb(31, 31, 31)",
+ "--sys-color-border-base": "rgb(119, 119, 119)",
+ "--sys-color-border-strong": "rgb(7, 7, 7)",
+ "--sys-color-border-subtle": "rgb(198, 198, 198)",
+ "--sys-color-feedback-error": "rgb(179, 52, 43)",
+ "--sys-color-feedback-info": "rgb(18, 103, 167)",
+ "--sys-color-feedback-success": "rgb(25, 113, 61)",
+ "--sys-color-feedback-warning": "rgb(138, 89, 0)",
+ "--sys-color-overlay-scrim": "rgba(0, 0, 0, 0.5)",
+ "--sys-color-surface-base": "rgb(255, 255, 255)",
+ "--sys-color-surface-base-hover": "rgb(238, 238, 238)",
+ "--sys-color-surface-danger": "rgb(130, 89, 85)",
+ "--sys-color-surface-danger-hover": "rgb(112, 73, 71)",
+ "--sys-color-surface-primary": "rgb(31, 31, 31)",
+ "--sys-color-surface-primary-hover": "rgb(48, 48, 48)",
+ "--sys-color-surface-subtle": "rgb(249, 249, 249)",
+ "--sys-color-surface-subtle-hover": "rgb(238, 238, 238)",
+ "--sys-color-text-base": "rgb(31, 31, 31)",
+ "--sys-color-text-on-danger": "rgb(249, 249, 249)",
+ "--sys-color-text-on-primary": "rgb(249, 249, 249)",
+ "--sys-color-text-subtle": "rgb(94, 94, 94)",
+ "--sys-elevation-raised-high-color": "rgba(0, 0, 0, 0.18)",
+ "--sys-elevation-raised-high-offset-x": "0px",
+ "--sys-elevation-raised-high-offset-y": "4px",
+ "--sys-elevation-raised-high-radius": "12px",
+ "--sys-elevation-raised-high-shadow-type": "dropShadow",
+ "--sys-elevation-raised-high-spread": "0px",
+ "--sys-elevation-raised-low-color": "rgba(0, 0, 0, 0.1)",
+ "--sys-elevation-raised-low-offset-x": "0px",
+ "--sys-elevation-raised-low-offset-y": "1px",
+ "--sys-elevation-raised-low-radius": "2px",
+ "--sys-elevation-raised-low-shadow-type": "dropShadow",
+ "--sys-elevation-raised-low-spread": "0px",
+ "--sys-elevation-raised-md-color": "rgba(0, 0, 0, 0.16)",
+ "--sys-elevation-raised-md-offset-x": "0px",
+ "--sys-elevation-raised-md-offset-y": "2px",
+ "--sys-elevation-raised-md-radius": "6px",
+ "--sys-elevation-raised-md-shadow-type": "dropShadow",
+ "--sys-elevation-raised-md-spread": "0px"
+ },
+ "shadows": [
+ {
+ "level": "low",
+ "shadow": "rgba(0, 0, 0, 0.1) 0px 1px 2px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px 0px"
+ },
+ {
+ "level": "md",
+ "shadow": "rgba(0, 0, 0, 0.16) 0px 2px 6px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.16) 0px 2px 6px 0px"
+ },
+ {
+ "level": "high",
+ "shadow": "rgba(0, 0, 0, 0.18) 0px 4px 12px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.18) 0px 4px 12px 0px"
+ }
+ ],
+ "buttons": "normal/hover/pressed/focus/disabled/pending",
+ "errorWithFocus": true,
+ "selectedLabelAndDescription": true,
+ "rootPopupAndScrim": true,
+ "openPopupThemeChange": true,
+ "sassRuntimeShadow": true,
+ "unoBuildTimeShadow": true,
+ "narrowWidth": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "matchedVariables": 41,
+ "tokens": {
+ "--comp-switch-handle-bg": "rgb(31, 31, 31)",
+ "--comp-switch-label-color": "rgb(226, 226, 226)",
+ "--comp-switch-track-bg": "rgb(226, 226, 226)",
+ "--sys-color-border-base": "rgb(145, 145, 145)",
+ "--sys-color-border-strong": "rgb(198, 198, 198)",
+ "--sys-color-border-subtle": "rgb(94, 94, 94)",
+ "--sys-color-feedback-error": "rgb(237, 150, 144)",
+ "--sys-color-feedback-info": "rgb(112, 181, 237)",
+ "--sys-color-feedback-success": "rgb(103, 200, 140)",
+ "--sys-color-feedback-warning": "rgb(229, 185, 81)",
+ "--sys-color-overlay-scrim": "rgba(0, 0, 0, 0.5)",
+ "--sys-color-surface-base": "rgb(31, 31, 31)",
+ "--sys-color-surface-base-hover": "rgb(48, 48, 48)",
+ "--sys-color-surface-danger": "rgb(200, 161, 157)",
+ "--sys-color-surface-danger-hover": "rgb(185, 145, 139)",
+ "--sys-color-surface-primary": "rgb(226, 226, 226)",
+ "--sys-color-surface-primary-hover": "rgb(198, 198, 198)",
+ "--sys-color-surface-subtle": "rgb(48, 48, 48)",
+ "--sys-color-surface-subtle-hover": "rgb(58, 58, 58)",
+ "--sys-color-text-base": "rgb(226, 226, 226)",
+ "--sys-color-text-on-danger": "rgb(31, 31, 31)",
+ "--sys-color-text-on-primary": "rgb(31, 31, 31)",
+ "--sys-color-text-subtle": "rgb(171, 171, 171)",
+ "--sys-elevation-raised-high-color": "rgba(0, 0, 0, 0.18)",
+ "--sys-elevation-raised-high-offset-x": "0px",
+ "--sys-elevation-raised-high-offset-y": "4px",
+ "--sys-elevation-raised-high-radius": "12px",
+ "--sys-elevation-raised-high-shadow-type": "dropShadow",
+ "--sys-elevation-raised-high-spread": "0px",
+ "--sys-elevation-raised-low-color": "rgba(0, 0, 0, 0.1)",
+ "--sys-elevation-raised-low-offset-x": "0px",
+ "--sys-elevation-raised-low-offset-y": "1px",
+ "--sys-elevation-raised-low-radius": "2px",
+ "--sys-elevation-raised-low-shadow-type": "dropShadow",
+ "--sys-elevation-raised-low-spread": "0px",
+ "--sys-elevation-raised-md-color": "rgba(0, 0, 0, 0.16)",
+ "--sys-elevation-raised-md-offset-x": "0px",
+ "--sys-elevation-raised-md-offset-y": "2px",
+ "--sys-elevation-raised-md-radius": "6px",
+ "--sys-elevation-raised-md-shadow-type": "dropShadow",
+ "--sys-elevation-raised-md-spread": "0px"
+ },
+ "shadows": [
+ {
+ "level": "low",
+ "shadow": "rgba(0, 0, 0, 0.1) 0px 1px 2px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px 0px"
+ },
+ {
+ "level": "md",
+ "shadow": "rgba(0, 0, 0, 0.16) 0px 2px 6px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.16) 0px 2px 6px 0px"
+ },
+ {
+ "level": "high",
+ "shadow": "rgba(0, 0, 0, 0.18) 0px 4px 12px 0px",
+ "uno": "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.18) 0px 4px 12px 0px"
+ }
+ ],
+ "buttons": "normal/hover/pressed/focus/disabled/pending",
+ "errorWithFocus": true,
+ "selectedLabelAndDescription": true,
+ "rootPopupAndScrim": true,
+ "openPopupThemeChange": true,
+ "sassRuntimeShadow": true,
+ "unoBuildTimeShadow": true,
+ "narrowWidth": 375,
+ "overflow": 0
+ }
+ ],
+ "errors": [],
+ "requests": []
+}
diff --git a/tasks/ui-foundations/i2-study/App.vue b/tasks/ui-foundations/i2-study/App.vue
new file mode 100644
index 0000000..ad53834
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/App.vue
@@ -0,0 +1,457 @@
+
+
+
+
+
+ 色彩与阴影 · 场景校准
+
+ 相同组件、内容与状态,只改变配色和阴影。候选尚未写入正式 Token;本页操作不保存业务数据。
+
+
+
+
+
+
+
+
+
+
+ {{ panel.title }}
+
+
+
+
+
阅读与记录
+
收集值得保留的内容,继续上次的阅读。
+
+ 阅读摘录3 分钟前更新
+
+
+ 每周通讯昨天更新
+
+
+ 采集设置
+
+
+
+
+
+
+
+
+ 正在保存设置…
+ {{ notice }}
+
+
+
+
+
动作与反馈
+
可悬停、按下或用 Tab 聚焦。等待中的动作保留原配色和文字。
+
+ {{ { primary: "主要动作", subtle: "次要动作", danger: "危险动作" }[kind] }}
+
+
+
+
+
+
+ {{ feedback[feedbackKind].title }}
+
+ {{ feedback[feedbackKind].detail }}
+
+
{{ notice }}
+
+
+
覆盖关系
+
+ 常驻内容不加阴影;提示覆盖内容时,用细边界和贴近轮廓的轻阴影区分。
+
+
+
+ 悬停或聚焦「连接说明」查看实际 Tooltip。下方并列展开三个阴影档位,便于比较范围。
+
+
+
+ {{ { low: "较弱", md: "默认", high: "较强" }[level] }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tasks/ui-foundations/i2-study/Delivery.vue b/tasks/ui-foundations/i2-study/Delivery.vue
new file mode 100644
index 0000000..2a8a01a
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/Delivery.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+ Sass · {{ level }}
+ Uno · {{ level }}
+
+
+ Uno 主动作配对
+
+ 移除阅读摘录?
+ 停止后续采集,已经保存的内容仍可阅读。
+
+
+
+
+
+
+ 阅读摘录
+ 当前内容保持完整,关闭后回到原来的采集设置。
+
+
+
+
+
+
diff --git a/tasks/ui-foundations/i2-study/README.md b/tasks/ui-foundations/i2-study/README.md
new file mode 100644
index 0000000..3ce3954
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/README.md
@@ -0,0 +1,77 @@
+# I2 色彩与阴影校准
+
+本页保留色彩与阴影的校准过程。用户现已认可修订版,选定值已写回 Token 规范源;正式源、实际产物与最终验证见 [I2 交付验收](../i2-evidence/README.md)。本目录的离线页面保留校准时的 I1/候选对照与提示文字,不作为当前发布状态。消费者仍使用 registry UI 2.0.0。
+
+打开 [交互对照](index.html),左侧是 I1 实际产物,右侧是修订候选。可以切换浅深主题,以及页面场景、动作与反馈、覆盖层阴影三个视图;表单另有正常、错误、等待和不可用状态。在“动作与反馈”中选择反馈情境,分别查看日常状态、连接验证成功、授权即将到期、采集范围变化和连接失败。两个面板共用相同内容和草稿,页面布局不作为新设计规则。本页不请求后端、不保存业务数据。
+
+## 这组候选改变的关系
+
+| 范围 | 现状 | 候选与理由 |
+| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
+| 主动作 | 浅深主题的主表面、悬停与反色文字带蓝灰倾向 | 改为中性灰配对;浅色用深灰动作,深色用浅灰动作,保留主次区别 |
+| 深色表面 | 基底 `#474747`,次要区域 `#1f1f1f`,悬停升到 `#5e5e5e` | 基底 `#1f1f1f`,次要区域 `#303030`,较亮的悬停止于 `#3a3a3a`,减少相邻表面的明暗跳变 |
+| 反馈 | I1 使用较鲜艳的前景;上一版候选的灰绿、灰赭和灰蓝被用户指出过于雾化 | 改用更干净的绿、金黄、蓝和红,只强调需要注意的关键文字;常态状态与详细说明保持中性色 |
+| 危险动作 | 两主题都以较鲜艳的深红底配浅色字 | 浅色用灰红底配浅字,深色用较亮的灰红底配深字;前景和表面一起变化 |
+| 次要文字与必要边界 | 深色边界、文字与较亮的 hover 表面没有统一配对余量 | 适当降低常态亮度,同时限制 hover 表面亮度,维持边界和文字的可辨认性 |
+| Switch | 轨道与滑块接近纯黑白 | 随主动作采用相近的中性灰范围,保留状态文字与滑块的对比 |
+| 覆盖层阴影 | 默认阴影向下偏移 6px、模糊 20px,颜色带蓝灰 | 默认阴影改为向下 2px、模糊 6px 的中性黑;范围收紧后保留足够的边缘层次 |
+
+所有候选值集中在 [palette.mjs](palette.mjs)。浅色普通表面继续使用现有白色/近白色;阴影三个档位分别是 1px/2px/4px 垂直偏移和 2px/6px/12px 模糊。较弱和较强的小样用于检查家族关系,不赋予常驻卡片使用阴影的许可。
+
+本次反馈前景如下。浅色主题使用较深的色值保留文字对比,深色主题提高明度;“较纯”不等于使用最高饱和度的 RGB 原色。错误文字与危险按钮底色分别承担用途,前者一同提高纯度,后者保留用户认可的灰红。
+
+| 反馈前景 | 浅色主题 | 深色主题 |
+| -------- | --------- | --------- |
+| success | `#19713d` | `#67c88c` |
+| warning | `#8a5900` | `#e5b951` |
+| info | `#1267a7` | `#70b5ed` |
+| error | `#b3342b` | `#ed9690` |
+
+页面场景已移除常驻的成功/警告着色,普通保存结果和等待说明也使用中性色。反馈示例随情境切换,只有关键文字着色,说明正文保持普通文字颜色;不再并列陈列四种反馈来示范实际用色密度。这是实验的对照安排,不限制真实页面只能出现一个问题,也不授权隐藏同时存在的风险。
+
+现行面板直接使用 I1 tarball 的根级主题,候选只覆盖已有系统颜色、Switch 颜色和阴影变量。组件也取自该 tarball 的真实 JS/CSS;没有重新解析或抄写现行色值,也没有按截图重写按钮、字段、开关、选择器或 Tooltip。示例不使用 Teleport 浮层,因此可以在两个面板并排比较;正式 Popup/Scrim 的候选覆盖仍须在根级验证。
+
+## 证据与边界
+
+2026-09-13 使用 Chromium 149.0.7827.55 完成 [浏览器与配对记录](verification.json)。每个主题/面板检查 29 组文字配对和 13 组边界配对,共 168 组;其中候选占 84 组。配对使用浏览器实际读取的变量,阈值判断保留未舍入值。
+
+| 配对范围 | 最低文字对比度 | 最低必要边界对比度 | 低于本次配对要求的组合 |
+| -------- | -------------- | ------------------ | ---------------------- |
+| 现行浅色 | 5.58:1 | 3.86:1 | 0 |
+| 候选浅色 | 5.14:1 | 3.86:1 | 0 |
+| 现行深色 | 3.66:1 | 2.82:1 | 10 |
+| 候选深色 | 4.95:1 | 3.61:1 | 0 |
+
+现行深色的缺口集中在 `base-hover`/`subtle-hover`:次要文字、success/info/error 前景及必要边界在这些表面上缺少对比。这里盘点的是允许组合的角色关系,不能仅凭组合存在就断言某个实际页面不合规;也不能把候选通过数值检查称为整体视觉认可。
+
+普通文字按 [WCAG 2.2 的最低文字对比说明](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) 检查 4.5:1;识别控件所需的边界和状态按 [非文本对比说明](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html) 检查 3:1。禁用控件的例外、装饰分隔与阴影不混作必要边界。可读性还受字体和场景影响,本次继续保持既有文字尺度与角色。
+
+真实组件操作通过以下复核:primary/subtle/danger 的普通、hover、pressed、键盘焦点和禁用状态;错误字段保留错误边框与独立焦点;等待动作保留主配色和文字;Tooltip 聚焦/悬停打开及 Escape 关闭。浅深主题的三个视图在 375px 下均无横向溢出,居中触发的提示也保持在视口内。页面脚本错误和 HTTP(S) 请求均为零,六张代表截图已人工查看。
+
+修订版另核对 20 组反馈情境:两种主题、两个面板、五种状态。日常状态使用中性色,四种需要注意的反馈只在关键文字上应用对应前景,说明正文保持 text.base。普通保存结果与等待说明的实际颜色也已验证为 text.subtle。更高的数值对比仍不替代用户对颜色纯度和观感的确认。
+
+截图只说明对应实验:页面[浅色](scene-light.png)/[深色](scene-dark.png),状态[浅色](states-light.png)/[深色](states-dark.png),阴影[浅色](layers-light.png)/[深色](layers-dark.png)。截图等待有限 CSS 过渡结束后获取;等待图标自身的循环动画不作为结束条件。
+
+## 复跑与正式落地
+
+UI 基线为 `11be433`,候选 tarball 仍标记 2.0.0,SHA-256 为 `7be8c129cdee364977199b592da15f37cfca2af3df30d9c408f0afa58967ca8b`;它不同于 registry 已发布的同版本。[artifact.json](artifact.json) 中的 source 指这一 UI 基线,实验源码和候选值仍是本轮未提交内容。
+
+在根目录准备本地 UI tarball,然后运行:
+
+```sh
+node tasks/ui-foundations/i2-study/build.mjs tmp/i1-pack/inkcre-ui-web-2.0.0.tgz
+node tasks/ui-foundations/i2-study/verify.mjs /path/to/existing/playwright/index.mjs
+```
+
+本机验证复用 client-web 迁移 worktree 已安装的 Playwright。构建复用现有 Vue/Vite,输出无需服务器的单文件 HTML,并清理临时解包目录。实验与脚本随工作包撤除,不加入组件测试套件、生产包或发布流程。
+
+本轮将用户明确的用途与用量规则同步到设计正文、Web 样式指南和 Agent 配方,并把两个普通提交反馈示例改为中性色。已生成随包副本并补 patch Changeset,完整 `pnpm check` 通过,包括实际 tarball 安装与文档/配方检查、21 个 Story/120 个 Variant 构建;日志为本机 `/tmp/inkcre-i2-feedback-check.log`。Token JSON、组件运行时和消费者源码没有改变。
+
+视觉反馈确认后,按下列边界落地:
+
+1. 将选定配对写回现有 `sys.light/dark`、`comp.light/dark` 和 `effect.elevation`,同步受影响的 ref 色族及引用;保留公共角色、类型和用途,避免留下与默认风格相反的品牌色族。
+2. 沿用现有生成链同步 Sass、CSS 和 Uno;Sass 阴影 helper 读取运行时变量,Uno 阴影仍是构建值,两种入口都要验证。
+3. 对正式 tarball 复核本轮涉及的配对与实际组件,并补根级 Popup/Scrim、深色选中项等依赖全局上下文的场景;更新适用指南与 Changeset,运行完整 `pnpm check`。
+4. 下游按后续已发布产物复验,保持正式依赖与源码实验的证据分开。页面内容层级仍由 I3 处理,不将配色校准扩成信息架构重做。
+
+用户已确认修订后的反馈文字纯度,并要求利用 Token 机制继续迭代,无须把每个色值视为不可更改的最终决定。上述正式落地现已完成并通过[实际产物验收](../i2-evidence/README.md),未提交或发布;消费者仍使用 registry UI 2.0.0。
diff --git a/tasks/ui-foundations/i2-study/artifact.json b/tasks/ui-foundations/i2-study/artifact.json
new file mode 100644
index 0000000..891a6cb
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/artifact.json
@@ -0,0 +1,5 @@
+{
+ "source": "11be433ca6e7c203da7f42233983e576a967d71e",
+ "package": "@inkcre/ui-web@2.0.0",
+ "tarballSha256": "7be8c129cdee364977199b592da15f37cfca2af3df30d9c408f0afa58967ca8b"
+}
diff --git a/tasks/ui-foundations/i2-study/build.mjs b/tasks/ui-foundations/i2-study/build.mjs
new file mode 100644
index 0000000..76a7f0d
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/build.mjs
@@ -0,0 +1,110 @@
+import { createRequire } from "node:module";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { resolve, dirname } from "node:path";
+import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { execFileSync } from "node:child_process";
+import { createHash } from "node:crypto";
+
+const here = dirname(fileURLToPath(import.meta.url));
+const root = resolve(here, "../../..");
+const require = createRequire(resolve(root, "packages/web/package.json"));
+const { build } = await import(require.resolve("vite"));
+const { default: vue } = await import(require.resolve("@vitejs/plugin-vue"));
+if (!process.argv[2]) throw new Error("Pass the local UI tarball used as the comparison baseline.");
+if (process.argv[3] && process.argv[3] !== "--delivery") throw new Error("Unknown build mode.");
+const delivery = process.argv[3] === "--delivery";
+const destination = delivery ? resolve(here, "../i2-evidence") : here;
+mkdirSync(destination, { recursive: true });
+const tarball = resolve(process.argv[2]);
+const archive = readFileSync(tarball);
+const scratchRoot = resolve(root, "packages/web/tmp");
+mkdirSync(scratchRoot, { recursive: true });
+const scratch = mkdtempSync(resolve(scratchRoot, "i2-study-"));
+try {
+ execFileSync("tar", ["-xzf", tarball, "-C", scratch]);
+ const packageRoot = resolve(scratch, "package");
+ const manifest = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
+ let probeCss = "";
+ if (delivery) {
+ const sass = require("sass");
+ probeCss = sass.compileString(
+ `
+ @use "mixins" as ink;
+ @each $level in low, md, high {
+ .sass-shadow-#{$level} { @include ink.apply-elevation($level); }
+ }
+ `,
+ { loadPaths: [resolve(packageRoot, "styles")] },
+ ).css;
+ const { createGenerator, presetWind3 } = await import(require.resolve("unocss"));
+ const { presetInk } = await import(
+ pathToFileURL(resolve(packageRoot, "dist/uno/preset-ink.js")).href
+ );
+ const uno = await createGenerator({ presets: [presetWind3(), presetInk()] });
+ probeCss += (
+ await uno.generate(
+ "shadow-low shadow-md shadow-high bg-surface-primary text-text-on-primary",
+ { preflights: true },
+ )
+ ).css;
+ }
+ const result = await build({
+ configFile: false,
+ root: here,
+ plugins: [vue()],
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
+ resolve: {
+ alias: [
+ { find: /^vue$/, replacement: require.resolve("vue/dist/vue.runtime.esm-bundler.js") },
+ { find: /^@inkcre\/ui-web$/, replacement: resolve(packageRoot, "dist/index.js") },
+ { find: /^@inkcre\/ui-web\/styles$/, replacement: resolve(packageRoot, "dist/index.css") },
+ ],
+ dedupe: ["vue"],
+ },
+ build: {
+ write: false,
+ lib: {
+ entry: resolve(here, delivery ? "delivery.js" : "main.js"),
+ formats: ["iife"],
+ name: "InkcreColorStudy",
+ },
+ minify: true,
+ sourcemap: false,
+ cssCodeSplit: false,
+ },
+ });
+ const output = (Array.isArray(result) ? result : [result]).flatMap((bundle) => bundle.output);
+ const js = output.find((item) => item.type === "chunk").code.replaceAll(" item.type === "asset" && item.fileName.endsWith(".css"))
+ .map((item) => item.source)
+ .join("\n");
+ writeFileSync(
+ resolve(destination, "index.html"),
+ `InKCre · I2 ${delivery ? "交付验收" : "色彩与阴影"}\n`,
+ );
+ writeFileSync(
+ resolve(destination, "artifact.json"),
+ JSON.stringify(
+ {
+ source: execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim(),
+ package: `${manifest.name}@${manifest.version}`,
+ tarballSha256: createHash("sha256").update(archive).digest("hex"),
+ ...(delivery
+ ? {
+ tokensSha256: createHash("sha256")
+ .update(readFileSync(resolve(root, "tokens/inkcre.tokens.json")))
+ .digest("hex"),
+ cssSha256: createHash("sha256")
+ .update(readFileSync(resolve(packageRoot, "dist/index.css")))
+ .digest("hex"),
+ }
+ : {}),
+ },
+ null,
+ 2,
+ ) + "\n",
+ );
+} finally {
+ rmSync(scratch, { recursive: true, force: true });
+}
diff --git a/tasks/ui-foundations/i2-study/delivery.js b/tasks/ui-foundations/i2-study/delivery.js
new file mode 100644
index 0000000..b1c94da
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/delivery.js
@@ -0,0 +1,4 @@
+import { createApp } from "vue";
+import Delivery from "./Delivery.vue";
+import "@inkcre/ui-web/styles";
+createApp(Delivery).mount("#app");
diff --git a/tasks/ui-foundations/i2-study/index.html b/tasks/ui-foundations/i2-study/index.html
new file mode 100644
index 0000000..b790371
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/index.html
@@ -0,0 +1,12 @@
+InKCre · I2 色彩与阴影
diff --git a/tasks/ui-foundations/i2-study/layers-dark.png b/tasks/ui-foundations/i2-study/layers-dark.png
new file mode 100644
index 0000000..1e52dfd
Binary files /dev/null and b/tasks/ui-foundations/i2-study/layers-dark.png differ
diff --git a/tasks/ui-foundations/i2-study/layers-light.png b/tasks/ui-foundations/i2-study/layers-light.png
new file mode 100644
index 0000000..3c4d00d
Binary files /dev/null and b/tasks/ui-foundations/i2-study/layers-light.png differ
diff --git a/tasks/ui-foundations/i2-study/main.js b/tasks/ui-foundations/i2-study/main.js
new file mode 100644
index 0000000..0741b01
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/main.js
@@ -0,0 +1,4 @@
+import { createApp } from "vue";
+import App from "./App.vue";
+import "@inkcre/ui-web/styles";
+createApp(App).mount("#app");
diff --git a/tasks/ui-foundations/i2-study/palette.mjs b/tasks/ui-foundations/i2-study/palette.mjs
new file mode 100644
index 0000000..d76ad43
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/palette.mjs
@@ -0,0 +1,65 @@
+export const candidate = {
+ light: {
+ "text-base": "#1f1f1f",
+ "text-on-primary": "#f9f9f9",
+ "text-on-danger": "#f9f9f9",
+ "surface-primary": "#1f1f1f",
+ "surface-primary-hover": "#303030",
+ "surface-danger": "#825955",
+ "surface-danger-hover": "#704947",
+ "feedback-success": "#19713d",
+ "feedback-warning": "#8a5900",
+ "feedback-info": "#1267a7",
+ "feedback-error": "#b3342b",
+ },
+ dark: {
+ "text-base": "#e2e2e2",
+ "text-subtle": "#ababab",
+ "text-on-primary": "#1f1f1f",
+ "text-on-danger": "#1f1f1f",
+ "surface-base": "#1f1f1f",
+ "surface-subtle": "#303030",
+ "surface-primary": "#e2e2e2",
+ "surface-primary-hover": "#c6c6c6",
+ "surface-subtle-hover": "#3a3a3a",
+ "surface-base-hover": "#303030",
+ "surface-danger": "#c8a19d",
+ "surface-danger-hover": "#b9918b",
+ "border-base": "#919191",
+ "border-subtle": "#5e5e5e",
+ "feedback-success": "#67c88c",
+ "feedback-warning": "#e5b951",
+ "feedback-info": "#70b5ed",
+ "feedback-error": "#ed9690",
+ },
+};
+
+export const candidateElevation = {
+ low: { offsetX: 0, offsetY: 1, radius: 2, spread: 0, color: "#0000001a" },
+ md: { offsetX: 0, offsetY: 2, radius: 6, spread: 0, color: "#00000029" },
+ high: { offsetX: 0, offsetY: 4, radius: 12, spread: 0, color: "#0000002e" },
+};
+
+export function sceneTokens(theme, proposed) {
+ if (!proposed) return {};
+ const variables = Object.fromEntries(
+ Object.entries(candidate[theme]).map(([name, value]) => [`--sys-color-${name}`, value]),
+ );
+ variables["--comp-switch-track-bg"] = theme === "light" ? "#1f1f1f" : "#e2e2e2";
+ variables["--comp-switch-handle-bg"] = theme === "light" ? "#f9f9f9" : "#1f1f1f";
+ variables["--comp-switch-label-color"] = variables["--comp-switch-track-bg"];
+ for (const level of ["low", "md", "high"]) {
+ const effect = candidateElevation[level];
+ for (const [key, field] of Object.entries({
+ "offset-x": "offsetX",
+ "offset-y": "offsetY",
+ radius: "radius",
+ spread: "spread",
+ color: "color",
+ })) {
+ variables[`--sys-elevation-raised-${level}-${key}`] =
+ field === "color" ? effect[field] : `${effect[field]}px`;
+ }
+ }
+ return variables;
+}
diff --git a/tasks/ui-foundations/i2-study/scene-dark.png b/tasks/ui-foundations/i2-study/scene-dark.png
new file mode 100644
index 0000000..8669998
Binary files /dev/null and b/tasks/ui-foundations/i2-study/scene-dark.png differ
diff --git a/tasks/ui-foundations/i2-study/scene-light.png b/tasks/ui-foundations/i2-study/scene-light.png
new file mode 100644
index 0000000..38cd5aa
Binary files /dev/null and b/tasks/ui-foundations/i2-study/scene-light.png differ
diff --git a/tasks/ui-foundations/i2-study/states-dark.png b/tasks/ui-foundations/i2-study/states-dark.png
new file mode 100644
index 0000000..ec46191
Binary files /dev/null and b/tasks/ui-foundations/i2-study/states-dark.png differ
diff --git a/tasks/ui-foundations/i2-study/states-light.png b/tasks/ui-foundations/i2-study/states-light.png
new file mode 100644
index 0000000..71953ce
Binary files /dev/null and b/tasks/ui-foundations/i2-study/states-light.png differ
diff --git a/tasks/ui-foundations/i2-study/verification.json b/tasks/ui-foundations/i2-study/verification.json
new file mode 100644
index 0000000..3ad5631
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/verification.json
@@ -0,0 +1,365 @@
+{
+ "source": "11be433ca6e7c203da7f42233983e576a967d71e",
+ "package": "@inkcre/ui-web@2.0.0",
+ "tarballSha256": "7be8c129cdee364977199b592da15f37cfca2af3df30d9c408f0afa58967ca8b",
+ "browser": "149.0.7827.55",
+ "colors": [
+ {
+ "theme": "light",
+ "palette": "current",
+ "textPairs": 29,
+ "boundaryPairs": 13,
+ "minimumText": {
+ "foreground": "--sys-color-feedback-error",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 5.581280277976265,
+ "threshold": 4.5
+ },
+ "minimumBoundary": {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.8596550990537786,
+ "threshold": 3
+ },
+ "failures": []
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "textPairs": 29,
+ "boundaryPairs": 13,
+ "minimumText": {
+ "foreground": "--sys-color-feedback-info",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 5.136425052803227,
+ "threshold": 4.5
+ },
+ "minimumBoundary": {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.8596550990537786,
+ "threshold": 3
+ },
+ "failures": []
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "textPairs": 29,
+ "boundaryPairs": 13,
+ "minimumText": {
+ "foreground": "--sys-color-feedback-success",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.6563483343098064,
+ "threshold": 4.5
+ },
+ "minimumBoundary": {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 2.823648221727756,
+ "threshold": 3
+ },
+ "failures": [
+ {
+ "foreground": "--sys-color-text-subtle",
+ "background": "--sys-color-surface-base-hover",
+ "ratio": 3.7960988661520703,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-success",
+ "background": "--sys-color-surface-base-hover",
+ "ratio": 3.6563483343098064,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-info",
+ "background": "--sys-color-surface-base-hover",
+ "ratio": 3.8195920127660896,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-error",
+ "background": "--sys-color-surface-base-hover",
+ "ratio": 3.808658019123771,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-text-subtle",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.7960988661520703,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-success",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.6563483343098064,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-info",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.8195920127660896,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-feedback-error",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.808658019123771,
+ "threshold": 4.5
+ },
+ {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-base-hover",
+ "ratio": 2.823648221727756,
+ "threshold": 3
+ },
+ {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 2.823648221727756,
+ "threshold": 3
+ }
+ ]
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "textPairs": 29,
+ "boundaryPairs": 13,
+ "minimumText": {
+ "foreground": "--sys-color-text-subtle",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 4.953236104038707,
+ "threshold": 4.5
+ },
+ "minimumBoundary": {
+ "foreground": "--sys-color-border-base",
+ "background": "--sys-color-surface-subtle-hover",
+ "ratio": 3.6089659792815545,
+ "threshold": 3
+ },
+ "failures": []
+ }
+ ],
+ "interactions": [
+ {
+ "theme": "light",
+ "errorBorderWithFocus": true,
+ "pendingKeepsPrimaryPair": true,
+ "routineStatusUsesNeutralText": true,
+ "buttonStates": "primary/subtle/danger: normal, hover, pressed, focus, disabled"
+ },
+ {
+ "theme": "dark",
+ "errorBorderWithFocus": true,
+ "pendingKeepsPrimaryPair": true,
+ "routineStatusUsesNeutralText": true,
+ "buttonStates": "primary/subtle/danger: normal, hover, pressed, focus, disabled"
+ }
+ ],
+ "feedback": [
+ {
+ "theme": "light",
+ "palette": "current",
+ "kind": "normal",
+ "titleColor": "#5e5e5e",
+ "detailColor": "#070707"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "kind": "normal",
+ "titleColor": "#5e5e5e",
+ "detailColor": "#1f1f1f"
+ },
+ {
+ "theme": "light",
+ "palette": "current",
+ "kind": "success",
+ "titleColor": "#176b39",
+ "detailColor": "#070707"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "kind": "success",
+ "titleColor": "#19713d",
+ "detailColor": "#1f1f1f"
+ },
+ {
+ "theme": "light",
+ "palette": "current",
+ "kind": "warning",
+ "titleColor": "#7a4b00",
+ "detailColor": "#070707"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "kind": "warning",
+ "titleColor": "#8a5900",
+ "detailColor": "#1f1f1f"
+ },
+ {
+ "theme": "light",
+ "palette": "current",
+ "kind": "info",
+ "titleColor": "#005c8a",
+ "detailColor": "#070707"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "kind": "info",
+ "titleColor": "#1267a7",
+ "detailColor": "#1f1f1f"
+ },
+ {
+ "theme": "light",
+ "palette": "current",
+ "kind": "error",
+ "titleColor": "#b9192f",
+ "detailColor": "#070707"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "kind": "error",
+ "titleColor": "#b3342b",
+ "detailColor": "#1f1f1f"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "kind": "normal",
+ "titleColor": "#c6c6c6",
+ "detailColor": "#f9f9f9"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "kind": "normal",
+ "titleColor": "#ababab",
+ "detailColor": "#e2e2e2"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "kind": "success",
+ "titleColor": "#75d69a",
+ "detailColor": "#f9f9f9"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "kind": "success",
+ "titleColor": "#67c88c",
+ "detailColor": "#e2e2e2"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "kind": "warning",
+ "titleColor": "#ffd27a",
+ "detailColor": "#f9f9f9"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "kind": "warning",
+ "titleColor": "#e5b951",
+ "detailColor": "#e2e2e2"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "kind": "info",
+ "titleColor": "#8bceff",
+ "detailColor": "#f9f9f9"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "kind": "info",
+ "titleColor": "#70b5ed",
+ "detailColor": "#e2e2e2"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "kind": "error",
+ "titleColor": "#ffb3b1",
+ "detailColor": "#f9f9f9"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "kind": "error",
+ "titleColor": "#ed9690",
+ "detailColor": "#e2e2e2"
+ }
+ ],
+ "shadows": [
+ {
+ "theme": "light",
+ "palette": "current",
+ "shadow": "rgba(12, 21, 48, 0.12) 0px 6px 20px 0px"
+ },
+ {
+ "theme": "light",
+ "palette": "candidate",
+ "shadow": "rgba(0, 0, 0, 0.16) 0px 2px 6px 0px"
+ },
+ {
+ "theme": "dark",
+ "palette": "current",
+ "shadow": "rgba(12, 21, 48, 0.12) 0px 6px 20px 0px"
+ },
+ {
+ "theme": "dark",
+ "palette": "candidate",
+ "shadow": "rgba(0, 0, 0, 0.16) 0px 2px 6px 0px"
+ }
+ ],
+ "narrow": [
+ {
+ "theme": "light",
+ "view": "scene",
+ "width": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "light",
+ "view": "states",
+ "width": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "light",
+ "view": "layers",
+ "width": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "view": "scene",
+ "width": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "view": "states",
+ "width": 375,
+ "overflow": 0
+ },
+ {
+ "theme": "dark",
+ "view": "layers",
+ "width": 375,
+ "overflow": 0
+ }
+ ],
+ "errors": [],
+ "networkRequests": []
+}
diff --git a/tasks/ui-foundations/i2-study/verify-delivery.mjs b/tasks/ui-foundations/i2-study/verify-delivery.mjs
new file mode 100644
index 0000000..00479be
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/verify-delivery.mjs
@@ -0,0 +1,274 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { createHash } from "node:crypto";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+
+if (!process.argv[2]) throw new Error("Pass an existing Playwright module path.");
+const { chromium, expect } = await import(pathToFileURL(resolve(process.argv[2])).href);
+const here = dirname(fileURLToPath(import.meta.url));
+const evidence = resolve(here, "../i2-evidence");
+const browser = await chromium.launch();
+const page = await browser.newPage({ viewport: { width: 1000, height: 1000 } });
+const study = await browser.newPage();
+const errors = [],
+ requests = [];
+for (const tab of [page, study]) {
+ tab.on("pageerror", (error) => errors.push(error.message));
+ tab.on("request", (request) => {
+ if (/^https?:/.test(request.url())) requests.push(request.url());
+ });
+}
+const report = {
+ ...JSON.parse(readFileSync(resolve(evidence, "artifact.json"), "utf8")),
+ approvedStudySha256: createHash("sha256")
+ .update(readFileSync(resolve(here, "index.html")))
+ .digest("hex"),
+ browser: browser.version(),
+ themes: [],
+};
+
+// 比较浏览器实际值;冻结样稿与产物分别构建,不从当前 Token 源重算期望值。
+const readTheme = (locator) =>
+ locator.evaluate((element) => {
+ const style = getComputedStyle(element);
+ const probe = document.createElement("span");
+ element.appendChild(probe);
+ const values = Object.fromEntries(
+ Array.from(style)
+ .filter(
+ (name) =>
+ name.startsWith("--sys-color-") ||
+ name.startsWith("--comp-switch-") ||
+ name.startsWith("--sys-elevation-raised-"),
+ )
+ .sort()
+ .map((name) => {
+ let value = style.getPropertyValue(name).trim();
+ if (CSS.supports("color", value)) {
+ probe.style.color = value;
+ value = getComputedStyle(probe).color;
+ }
+ return [name, value];
+ }),
+ );
+ probe.remove();
+ return values;
+ });
+const settle = () =>
+ page.waitForFunction(() =>
+ document
+ .getAnimations()
+ .every(
+ (animation) =>
+ animation.effect?.getComputedTiming().iterations === Infinity ||
+ animation.playState !== "running",
+ ),
+ );
+
+try {
+ await study.goto(pathToFileURL(resolve(here, "index.html")).href);
+ const expected = {};
+ for (const theme of ["light", "dark"]) {
+ await study.getByLabel("主题", { exact: true }).selectOption(theme);
+ await study.getByLabel("查看", { exact: true }).selectOption("layers");
+ const panel = study.locator('[data-palette="candidate"]');
+ expected[theme] = {
+ tokens: await readTheme(panel),
+ shadows: await panel
+ .locator(".shadow-sample")
+ .evaluateAll((elements) => elements.map((element) => getComputedStyle(element).boxShadow)),
+ };
+ }
+ await page.goto(pathToFileURL(resolve(evidence, "index.html")).href);
+ for (const theme of ["light", "dark"]) {
+ await page.getByLabel("主题", { exact: true }).selectOption(theme);
+ const tokens = await readTheme(page.locator("html"));
+ expect(tokens).toEqual(expected[theme].tokens);
+ const color = (role) => tokens[`--sys-color-${role}`];
+ for (const kind of ["primary", "subtle", "danger"]) {
+ const buttons = page.locator(`[data-kind="${kind}"] button`);
+ const button = buttons.first();
+ const foreground = kind === "subtle" ? "text-base" : `text-on-${kind}`;
+ await page.mouse.move(0, 0);
+ await expect(button).toHaveCSS("background-color", color(`surface-${kind}`));
+ await expect(button).toHaveCSS("color", color(foreground));
+ await button.hover();
+ await expect(button).toHaveCSS("background-color", color(`surface-${kind}-hover`));
+ await page.mouse.down();
+ await expect(button).toHaveCSS("background-color", color(`surface-${kind}-hover`));
+ await page.mouse.up();
+ await button.focus();
+ await button.press("Tab");
+ await page.keyboard.press("Shift+Tab");
+ await expect(button).toBeFocused();
+ await expect(button).toHaveCSS("outline-color", color("border-strong"));
+ await expect(button).toHaveCSS("outline-width", "2px");
+ await expect(buttons.nth(1)).toBeDisabled();
+ await expect(buttons.nth(1)).toHaveCSS("background-color", color("surface-subtle"));
+ await expect(buttons.nth(1)).toHaveCSS("color", color("text-subtle"));
+ await expect(buttons.nth(2)).toBeDisabled();
+ await expect(buttons.nth(2)).toHaveCSS("background-color", color(`surface-${kind}`));
+ await expect(buttons.nth(2)).toHaveCSS("color", color(foreground));
+ }
+ const input = page.getByRole("textbox", { name: "来源名称", exact: true });
+ await input.focus();
+ await input.press("ArrowLeft");
+ await expect(input).toHaveAttribute("aria-invalid", "true");
+ await expect(page.locator(".ink-input")).toHaveCSS("border-color", color("feedback-error"));
+ await expect(page.locator(".ink-input")).toHaveCSS("outline-color", color("border-strong"));
+ const combo = page.getByRole("combobox", { name: "保存内容", exact: true });
+ await combo.focus();
+ await combo.press("ArrowDown");
+ const listbox = page.getByRole("listbox", { name: "保存内容", exact: true });
+ const selected = listbox.getByRole("option", { selected: true });
+ await expect(selected).toHaveCSS("background-color", color("surface-primary-hover"));
+ for (const selector of [".option__label", ".option__description"]) {
+ await expect(selected.locator(selector)).toHaveCSS("color", color("text-on-primary"));
+ }
+ await combo.press("ArrowDown");
+ await expect(selected).toHaveCSS("background-color", color("surface-primary"));
+ const next = listbox.getByRole("option", { selected: false });
+ await expect(next).toHaveCSS("background-color", color("surface-base-hover"));
+ await expect(next.locator(".option__description")).toHaveCSS("color", color("text-subtle"));
+ await page.mouse.move(0, 0);
+ await settle();
+ await page.screenshot({ path: resolve(evidence, `controls-${theme}.png`), fullPage: true });
+ await combo.press("Escape");
+ await expect(combo).toBeFocused();
+
+ const shadows = [];
+ for (const [index, level] of ["low", "md", "high"].entries()) {
+ const sass = page.locator(`.sass-shadow-${level}`);
+ const uno = page.locator(`.shadow-${level}`);
+ await expect(sass).toHaveCSS("box-shadow", expected[theme].shadows[index]);
+ const unoShadow = await uno.evaluate((element) => getComputedStyle(element).boxShadow);
+ // Wind3 合成透明的 ring 层;比较可见阴影,同时保留完整计算值。
+ const painted = unoShadow
+ .split(/,(?![^(]*\))/)
+ .map((layer) => layer.trim())
+ .filter((layer) => !layer.startsWith("rgba(0, 0, 0, 0)"));
+ expect(painted).toEqual([expected[theme].shadows[index]]);
+ shadows.push({ level, shadow: expected[theme].shadows[index], uno: unoShadow });
+ }
+ const tooltipTrigger = page.getByRole("button", { name: "连接说明", exact: true });
+ await tooltipTrigger.focus();
+ const tooltip = page.locator('[role="tooltip"]');
+ await expect(tooltip).toBeVisible();
+ await expect(tooltip).toHaveCSS("box-shadow", expected[theme].shadows[1]);
+ await page.evaluate(() =>
+ document.documentElement.style.setProperty("--sys-elevation-raised-md-radius", "8px"),
+ );
+ await expect(page.locator(".sass-shadow-md")).not.toHaveCSS(
+ "box-shadow",
+ expected[theme].shadows[1],
+ );
+ await expect(tooltip).toHaveCSS(
+ "box-shadow",
+ await page
+ .locator(".sass-shadow-md")
+ .evaluate((element) => getComputedStyle(element).boxShadow),
+ );
+ await expect(page.locator(".shadow-md")).toHaveCSS("box-shadow", shadows[1].uno);
+ await page.evaluate(() =>
+ document.documentElement.style.removeProperty("--sys-elevation-raised-md-radius"),
+ );
+ await tooltipTrigger.press("Escape");
+ await expect(tooltip).toHaveAttribute("aria-hidden", "true");
+ await expect(page.locator(".uno-pair")).toHaveCSS("background-color", color("surface-primary"));
+ await expect(page.locator(".uno-pair")).toHaveCSS("color", color("text-on-primary"));
+
+ await page.getByRole("button", { name: "打开确认", exact: true }).click();
+ const popup = page.getByRole("dialog", { name: "设置确认", exact: true });
+ await expect(popup).toBeVisible();
+ expect(await popup.evaluate((element) => element.parentElement === document.body)).toBe(true);
+ await expect(popup).toHaveCSS("background-color", color("surface-base"));
+ await expect(popup).toHaveCSS("color", color("text-base"));
+ expect(
+ await popup.evaluate((element) => getComputedStyle(element, "::backdrop").backgroundColor),
+ ).toBe(color("overlay-scrim"));
+ const other = theme === "light" ? "dark" : "light";
+ await page.evaluate((value) => {
+ document.documentElement.dataset.theme = value;
+ }, other);
+ await expect(popup).toHaveCSS(
+ "background-color",
+ expected[other].tokens["--sys-color-surface-base"],
+ );
+ await expect(popup).toHaveCSS("color", expected[other].tokens["--sys-color-text-base"]);
+ await page.evaluate((value) => {
+ document.documentElement.dataset.theme = value;
+ }, theme);
+ await page.setViewportSize({ width: 375, height: 900 });
+ const popupBounds = await popup.boundingBox();
+ expect(popupBounds.x).toBeGreaterThanOrEqual(0);
+ expect(popupBounds.x + popupBounds.width).toBeLessThanOrEqual(375);
+ for (const button of await popup.getByRole("button").all()) {
+ const bounds = await button.boundingBox();
+ expect(bounds.x).toBeGreaterThanOrEqual(0);
+ expect(bounds.x + bounds.width).toBeLessThanOrEqual(375);
+ }
+ await settle();
+ await page.screenshot({ path: resolve(evidence, `popup-${theme}-375.png`) });
+ await page.keyboard.press("Escape");
+ await expect(popup).not.toBeVisible();
+ await expect(page.getByRole("button", { name: "打开确认", exact: true })).toBeFocused();
+
+ await page.getByRole("button", { name: "打开阅读层", exact: true }).click();
+ const scrim = page.getByRole("dialog", { name: "阅读内容", exact: true });
+ await expect(scrim).toBeVisible();
+ expect(await scrim.evaluate((element) => element.parentElement === document.body)).toBe(true);
+ await expect(scrim).toHaveCSS("background-color", color("overlay-scrim"));
+ await expect(scrim.locator(".scrim-content")).toHaveCSS(
+ "background-color",
+ color("surface-base"),
+ );
+ await expect(scrim.locator(".scrim-content")).toHaveCSS("color", color("text-base"));
+ await expect(scrim.getByRole("button", { name: "Close", exact: true })).toHaveCSS(
+ "background-color",
+ color("surface-subtle"),
+ );
+ await settle();
+ await page.screenshot({ path: resolve(evidence, `scrim-${theme}-375.png`) });
+ await scrim.getByRole("button", { name: "Close", exact: true }).click();
+ await expect(scrim).not.toBeVisible();
+ await expect(page.getByRole("button", { name: "打开阅读层", exact: true })).toBeFocused();
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - innerWidth);
+ expect(overflow).toBeLessThanOrEqual(1);
+ await page.setViewportSize({ width: 1000, height: 1000 });
+ report.themes.push({
+ theme,
+ matchedVariables: Object.keys(tokens).length,
+ tokens,
+ shadows,
+ buttons: "normal/hover/pressed/focus/disabled/pending",
+ errorWithFocus: true,
+ selectedLabelAndDescription: true,
+ rootPopupAndScrim: true,
+ openPopupThemeChange: true,
+ sassRuntimeShadow: true,
+ unoBuildTimeShadow: true,
+ narrowWidth: 375,
+ overflow,
+ });
+ }
+ expect(errors).toEqual([]);
+ expect(requests).toEqual([]);
+ writeFileSync(
+ resolve(evidence, "verification.json"),
+ JSON.stringify({ ...report, errors, requests }, null, 2) + "\n",
+ );
+ console.log(
+ JSON.stringify(
+ report.themes.map(({ theme, matchedVariables, overflow }) => ({
+ theme,
+ matchedVariables,
+ overflow,
+ })),
+ ),
+ );
+} catch (error) {
+ await page.screenshot({ path: resolve(evidence, "failure.png"), fullPage: true });
+ throw error;
+} finally {
+ await browser.close();
+}
diff --git a/tasks/ui-foundations/i2-study/verify.mjs b/tasks/ui-foundations/i2-study/verify.mjs
new file mode 100644
index 0000000..0a5bcf8
--- /dev/null
+++ b/tasks/ui-foundations/i2-study/verify.mjs
@@ -0,0 +1,296 @@
+import { pathToFileURL, fileURLToPath } from "node:url";
+import { dirname, resolve } from "node:path";
+import { readFileSync, writeFileSync } from "node:fs";
+
+if (!process.argv[2]) throw new Error("Pass an existing Playwright module path.");
+const { chromium, expect } = await import(pathToFileURL(resolve(process.argv[2])).href);
+const here = dirname(fileURLToPath(import.meta.url));
+const rgb = (hex) =>
+ hex
+ .slice(1, 7)
+ .match(/../g)
+ .map((part) => parseInt(part, 16));
+const cssRgb = (hex) => `rgb(${rgb(hex).join(", ")})`;
+function luminance(hex) {
+ const [r, g, b] = rgb(hex)
+ .map((value) => value / 255)
+ .map((value) => (value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4));
+ return r * 0.2126 + g * 0.7152 + b * 0.0722;
+}
+function contrast(a, b) {
+ const [x, y] = [luminance(a), luminance(b)];
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
+}
+function measurePairs(colors) {
+ const text = [],
+ boundaries = [];
+ const add = (list, foreground, background, threshold) => {
+ const ratio = contrast(colors[foreground], colors[background]);
+ list.push({ foreground, background, ratio, threshold });
+ };
+ for (const surface of ["base", "subtle", "base-hover", "subtle-hover"]) {
+ for (const role of [
+ "text-base",
+ "text-subtle",
+ "feedback-success",
+ "feedback-warning",
+ "feedback-info",
+ "feedback-error",
+ ])
+ add(text, `--sys-color-${role}`, `--sys-color-surface-${surface}`, 4.5);
+ for (const role of ["border-base", "border-strong", "feedback-error"])
+ add(boundaries, `--sys-color-${role}`, `--sys-color-surface-${surface}`, 3);
+ }
+ for (const role of ["primary", "danger"])
+ for (const state of ["", "-hover"])
+ add(text, `--sys-color-text-on-${role}`, `--sys-color-surface-${role}${state}`, 4.5);
+ add(text, "--comp-switch-label-color", "--comp-switch-handle-bg", 4.5);
+ add(boundaries, "--comp-switch-track-bg", "--comp-switch-handle-bg", 3);
+ const minimum = (list) => list.reduce((a, b) => (a.ratio < b.ratio ? a : b));
+ return {
+ textPairs: text.length,
+ boundaryPairs: boundaries.length,
+ minimumText: minimum(text),
+ minimumBoundary: minimum(boundaries),
+ failures: [...text, ...boundaries].filter((pair) => pair.ratio < pair.threshold),
+ };
+}
+
+const browser = await chromium.launch();
+const page = await browser.newPage({ viewport: { width: 1280, height: 1080 } });
+const errors = [],
+ requests = [];
+page.on("pageerror", (error) => errors.push(error.message));
+page.on("request", (request) => {
+ if (/^https?:/.test(request.url())) requests.push(request.url());
+});
+const report = {
+ ...JSON.parse(readFileSync(resolve(here, "artifact.json"), "utf8")),
+ browser: browser.version(),
+ colors: [],
+ interactions: [],
+ feedback: [],
+ shadows: [],
+ narrow: [],
+};
+const settle = () =>
+ page.waitForFunction(() =>
+ document
+ .getAnimations()
+ .every(
+ (animation) =>
+ animation.effect?.getComputedTiming().iterations === Infinity ||
+ animation.playState !== "running",
+ ),
+ );
+const paletteOf = (panel) =>
+ panel.evaluate((element) =>
+ Object.fromEntries(
+ Array.from(getComputedStyle(element))
+ .filter((name) => name.startsWith("--sys-color-") || name.startsWith("--comp-switch-"))
+ .map((name) => [name, getComputedStyle(element).getPropertyValue(name).trim()]),
+ ),
+ );
+try {
+ await page.goto(pathToFileURL(resolve(here, "index.html")).href);
+ await expect(page.locator("[data-palette]")).toHaveCount(2);
+ for (const theme of ["light", "dark"]) {
+ await page.getByLabel("主题", { exact: true }).selectOption(theme);
+ await page.getByLabel("查看", { exact: true }).selectOption("scene");
+ await page
+ .locator('[data-palette="current"]')
+ .getByRole("button", { name: "取消", exact: true })
+ .click();
+ await page.mouse.move(0, 0);
+ await settle();
+ await page.screenshot({ path: resolve(here, `scene-${theme}.png`), fullPage: true });
+ for (const id of ["current", "candidate"]) {
+ const panel = page.locator(`[data-palette="${id}"]`);
+ const colors = await paletteOf(panel);
+ const measured = measurePairs(colors);
+ report.colors.push({ theme, palette: id, ...measured });
+ if (id === "candidate") expect(measured.failures).toEqual([]);
+ await expect(
+ panel.locator(".source-row .success, .source-row .warning, .source-row .info"),
+ ).toHaveCount(0);
+ await panel.getByRole("button", { name: "保存设置", exact: true }).click();
+ await expect(panel.getByRole("status")).toHaveCSS(
+ "color",
+ cssRgb(colors["--sys-color-text-subtle"]),
+ );
+ }
+ await page.getByLabel("表单状态", { exact: true }).selectOption("error");
+ for (const id of ["current", "candidate"]) {
+ const panel = page.locator(`[data-palette="${id}"]`);
+ const input = panel.getByRole("textbox", { name: "来源名称", exact: true });
+ await input.focus();
+ await input.press("ArrowLeft");
+ await expect(input).toHaveAttribute("aria-invalid", "true");
+ const colors = await paletteOf(panel);
+ await expect(panel.locator(".ink-input")).toHaveCSS(
+ "border-color",
+ cssRgb(colors["--sys-color-feedback-error"]),
+ );
+ await expect(panel.locator(".ink-input")).toHaveCSS(
+ "outline-color",
+ cssRgb(colors["--sys-color-border-strong"]),
+ );
+ await expect(panel.locator(".ink-input")).toHaveCSS("outline-width", "2px");
+ }
+ await page.getByLabel("表单状态", { exact: true }).selectOption("pending");
+ for (const id of ["current", "candidate"]) {
+ const panel = page.locator(`[data-palette="${id}"]`);
+ const button = panel.getByRole("button", { name: "保存设置", exact: true });
+ const colors = await paletteOf(panel);
+ await expect(button).toBeDisabled();
+ await expect(button).toHaveCSS(
+ "background-color",
+ cssRgb(colors["--sys-color-surface-primary"]),
+ );
+ await expect(button).toHaveCSS("color", cssRgb(colors["--sys-color-text-on-primary"]));
+ await expect(panel.getByRole("status")).toHaveCSS(
+ "color",
+ cssRgb(colors["--sys-color-text-subtle"]),
+ );
+ }
+ await page.getByLabel("表单状态", { exact: true }).selectOption("normal");
+ await page.getByLabel("查看", { exact: true }).selectOption("states");
+ for (const kind of ["normal", "success", "warning", "info", "error"]) {
+ await page.getByLabel("反馈情境", { exact: true }).selectOption(kind);
+ for (const id of ["current", "candidate"]) {
+ const panel = page.locator(`[data-palette="${id}"]`);
+ const colors = await paletteOf(panel);
+ const feedback = panel.locator(".feedback-example");
+ await expect(feedback).toHaveCount(1);
+ await expect(feedback).toHaveAttribute("data-feedback", kind);
+ const titleColor =
+ colors[kind === "normal" ? "--sys-color-text-subtle" : `--sys-color-feedback-${kind}`];
+ await expect(feedback.locator(".feedback-title")).toHaveCSS("color", cssRgb(titleColor));
+ await expect(feedback.locator(".feedback-detail")).toHaveCSS(
+ "color",
+ cssRgb(colors["--sys-color-text-base"]),
+ );
+ report.feedback.push({
+ theme,
+ palette: id,
+ kind,
+ titleColor,
+ detailColor: colors["--sys-color-text-base"],
+ });
+ }
+ }
+ await page.getByLabel("反馈情境", { exact: true }).selectOption("warning");
+ await page.getByLabel("查看", { exact: true }).selectOption("scene");
+ await page
+ .locator('[data-palette="current"]')
+ .getByRole("button", { name: "取消", exact: true })
+ .click();
+ await page.getByLabel("查看", { exact: true }).selectOption("states");
+ await page.mouse.move(0, 0);
+ await settle();
+ await page.screenshot({ path: resolve(here, `states-${theme}.png`), fullPage: true });
+ for (const id of ["current", "candidate"])
+ for (const kind of ["primary", "subtle", "danger"]) {
+ const panel = page.locator(`[data-palette="${id}"]`);
+ const colors = await paletteOf(panel);
+ const button = panel.locator(`[data-kind="${kind}"] button`).first();
+ await page.mouse.move(0, 0);
+ await expect(button).toHaveCSS(
+ "background-color",
+ cssRgb(colors[`--sys-color-surface-${kind}`]),
+ );
+ await button.hover();
+ await expect(button).toHaveCSS(
+ "background-color",
+ cssRgb(colors[`--sys-color-surface-${kind}-hover`]),
+ );
+ await page.mouse.down();
+ await expect(button).toHaveCSS(
+ "background-color",
+ cssRgb(colors[`--sys-color-surface-${kind}-hover`]),
+ );
+ await page.mouse.up();
+ await button.focus();
+ await page.keyboard.press("Tab");
+ await page.keyboard.press("Shift+Tab");
+ await expect(button).toBeFocused();
+ await expect(button).toHaveCSS("outline-width", "2px");
+ const disabled = panel.locator(`[data-kind="${kind}"] button`).nth(1);
+ await expect(disabled).toBeDisabled();
+ await expect(disabled).toHaveCSS(
+ "background-color",
+ cssRgb(colors["--sys-color-surface-subtle"]),
+ );
+ }
+ report.interactions.push({
+ theme,
+ errorBorderWithFocus: true,
+ pendingKeepsPrimaryPair: true,
+ routineStatusUsesNeutralText: true,
+ buttonStates: "primary/subtle/danger: normal, hover, pressed, focus, disabled",
+ });
+ await page.getByLabel("查看", { exact: true }).selectOption("layers");
+ const buttons = page.getByRole("button", { name: "连接说明", exact: true });
+ await buttons.first().focus();
+ await buttons.nth(1).hover();
+ await settle();
+ for (const id of ["current", "candidate"]) {
+ const tooltip = page.locator(`[data-palette="${id}"] [role="tooltip"]`);
+ await expect(tooltip).toBeVisible();
+ await expect(tooltip).toHaveAttribute("aria-hidden", "false");
+ report.shadows.push({
+ theme,
+ palette: id,
+ shadow: await tooltip.evaluate((element) => getComputedStyle(element).boxShadow),
+ });
+ }
+ await page.screenshot({ path: resolve(here, `layers-${theme}.png`), fullPage: true });
+ await buttons.nth(1).focus();
+ await page.keyboard.press("Escape");
+ await expect(page.locator('[data-palette="candidate"] [role="tooltip"]')).toHaveAttribute(
+ "aria-hidden",
+ "true",
+ );
+ await page.setViewportSize({ width: 375, height: 900 });
+ for (const view of ["scene", "states", "layers"]) {
+ await page.getByLabel("查看", { exact: true }).selectOption(view);
+ if (view === "layers") {
+ await page
+ .locator('[data-palette="candidate"]')
+ .getByRole("button", { name: "连接说明", exact: true })
+ .focus();
+ const bounds = await page
+ .locator('[data-palette="candidate"] [role="tooltip"]')
+ .boundingBox();
+ expect(bounds.x).toBeGreaterThanOrEqual(0);
+ expect(bounds.x + bounds.width).toBeLessThanOrEqual(375);
+ }
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - innerWidth);
+ expect(overflow).toBeLessThanOrEqual(1);
+ report.narrow.push({ theme, view, width: 375, overflow });
+ }
+ await page.setViewportSize({ width: 1280, height: 1080 });
+ }
+ expect(errors).toEqual([]);
+ expect(requests).toEqual([]);
+ writeFileSync(
+ resolve(here, "verification.json"),
+ JSON.stringify({ ...report, errors, networkRequests: requests }, null, 2) + "\n",
+ );
+ console.log(
+ JSON.stringify(
+ report.colors.map(({ theme, palette, minimumText, minimumBoundary, failures }) => ({
+ theme,
+ palette,
+ minimumText: minimumText.ratio,
+ minimumBoundary: minimumBoundary.ratio,
+ failures: failures.length,
+ })),
+ ),
+ );
+} catch (error) {
+ await page.screenshot({ path: resolve(here, "failure.png"), fullPage: true });
+ throw error;
+} finally {
+ await browser.close();
+}
diff --git a/tasks/ui-foundations/packet.md b/tasks/ui-foundations/packet.md
index 96947bb..cbffd0d 100644
--- a/tasks/ui-foundations/packet.md
+++ b/tasks/ui-foundations/packet.md
@@ -10,6 +10,8 @@
功能与版本分别经 UI [PR #43](https://github.com/InKCre/ui/pull/43)、[PR #44](https://github.com/InKCre/ui/pull/44) 检查后合入。正式发布、产物身份和消费者关联见 [发布与真实迁移](release-and-consumer-migration.md)。本地历史提交和验证证据保留在后续执行记录中;“未提交/未发布/消费者原状”仅描述各阶段结束时的状态。
+2026-09-13 用户进一步指出,DESIGN.md 的缺口在于侧重点与知识组织,而不是是否禁止圆角。F0 已按总纲、设计立场、视觉语言、页面组合和判断依据完成重组,并通过逐项讨论确认主要视觉方向与先取舍内容再安排留白的原则。设计知识 `1e9a4a5`、I1 `11be433`、I2 `284ade6` 保留独立提交,已推送至 [UI PR #46](https://github.com/InKCre/ui/pull/46)。下游 I1 `6a61637`、I3 `9af7a17` 及数据库冷启动修复 `cfd7b12` 已推送至 PR #104,真实 preview 页面验收、本机 SSH 数据库 E2E 与最终远端 CI 均通过。UI 展示构建在 preview 中发现既有运行时替换问题,已独立提交 `9128425` 修复,完整检查和远端复验均通过,证据见文末。两个 PR 尚未合入,生产者新包未发布;消费者仍安装正式 UI 2.0.0。
+
DESIGN.md 的定位和实施方案见 [L3 讨论方案](plan-l3-design-docs.md),交付结果见 [L3 执行记录](l3-execution.md)。用户已授权发布 UI 和跨仓迁移,当前工作沿此边界继续;按用户要求,不恢复 Firefox 原生文字放大专项。
[A 组](groups.md#a-开发约定与文档入口) 的指南、命令、链接和历史信息校准已完成,已补 patch Changeset 并通过完整验证。后续执行见 [B—E 实施记录](execution-b-e.md)。
@@ -48,7 +50,7 @@ L0 实现基线已更新到 `origin/main` 的 `9f7267cbc410aa1421cd5d42ff5af2775
| L2 Web 组件 | [C](groups.md#c-基础控件与表单上下文) | 原生控件、字段关联、受控状态和基础事件 | 已验证、已提交 `b9c0bc6` |
| L2 Web 组件 | [D](groups.md#d-浮层确认与日期交互) | 模态与焦点、确认取消、异步状态、日期草稿 | 已验证、已提交 `b9c0bc6` |
| L2 Web 组件 | [E](groups.md#e-schema-表单与-json-编辑) | 用户数据、schema 隔离、校验、原始 JSON 编辑 | 已验证、已提交 `b9c0bc6` |
-| L3 文档与工程交付 | [F](groups.md#f-ai-消费文档与示例) | 共同设计指南、完整 API 事实、可检查的示例、Agent 读取入口 | F0—F3 已验证,已随 2.0.0 发布 |
+| L3 文档与工程交付 | [F](groups.md#f-ai-消费文档与示例) | 共同设计指南、完整 API 事实、可检查的示例、Agent 读取入口 | F0—F3 已随 2.0.0 发布;F0 重组与主要视觉方向已确认,本地交付验证通过 |
| L3 文档与工程交付 | [G](groups.md#g-package-工程验证与下游接入) | 实际消费者、依赖责任、最小安装、交付验证 | G2 与本地交付已发布;G3 在 client-web #104 验证,G1 外部位置仍有缺口 |
| L4 平台演进 | [H](groups.md#h-多端契约与扩展时机) | 共享语义与平台实现边界 | 原则已记录;具体平台方案未展开,实现暂缓 |
@@ -97,7 +99,7 @@ client-web、mail、twitter 与 ext-dev-utils 在独立迁移 worktree 中均锁
## L0 执行结果
-2026-09-12,A1、A2 已验证。实际改动仅涉及根级与包级 README/AGENTS、组件编写指南、现有文档导航、Token 与 Skill 生成说明、样式说明、Migration、本工作包和一份 [patch Changeset](../../.changeset/puny-clocks-boil.md)。L0 的运行时、生成器、依赖、工作流和生成物未发生差异;已提交为 `c5b230a`,未推送、发布或修改下游仓库。
+2026-09-12,A1、A2 已验证。实际改动仅涉及根级与包级 README/AGENTS、组件编写指南、现有文档导航、Token 与 Skill 生成说明、样式说明、Migration、本工作包和一份 [patch Changeset](https://github.com/InKCre/ui/blob/2b553ab002483474a603e691244019d0b5c628e5/.changeset/puny-clocks-boil.md)。L0 的运行时、生成器、依赖、工作流和生成物未发生差异;已提交为 `c5b230a`,未推送、发布或修改下游仓库。
移除了不存在的测试命令、逐组件测试文件要求、不准确的示例与旧路径;校正 `unknown`、原生元素、模型所有权及错误处理指导;明确 Story/Skill 检查能证明什么。Token 文档如实描述 `type/value` 输入与现有转换限制,没有迁移格式或更改 Token。消费者文档保留公开导入、直接 Markdown 入口及显式 Intent 配置,并将 client-web 联调链接更新到其当前开发运行时文档。
@@ -146,3 +148,221 @@ B5 仓库内迁移已验证,详见 [实施记录](b5-execution.md) 和 [正式
用户确认共同 design.md 的方案并要求开始 L3,现已完成本仓库 F0—F3、G2 和 G3 本地交付。根级 DESIGN.md、生成的模型/类型/默认值/事件/插槽文档、Story 共用配方、同版本随包发现、依赖责任与严格独立安装检查已同步。完整 pnpm check、最终 tarball 检查和三份配方的浏览器旅程通过;生成器对过期指南和未知 prop helper 按预期失败。结果见 [L3 执行记录](l3-execution.md) 与 [验证证据](l3-evidence/README.md)。
Vue 最低版本按真实声明校准为 3.5.25,内部 JSON 服务固定 5.6.4,已补 major Changeset;包版本仍为 1.4.0,未提交、推送或发布。G1 已核对三个本地消费者并记录实际调用缺口;extension registry 独立 UI/Figma 发送端尚缺位置,G3 真实消费者迁移和页面验收尚未完成。准备的局部跨仓补丁未应用,消费者 checkout 保持原状。
+
+## F0 复审:设计理念与视觉风格
+
+用户纠正了此前将下游视觉偏差收窄为“圆角默认及例外”的判断。当前问题是共同 DESIGN.md 是否充分表达 InKCre 的设计理念、视觉风格及其理由;不把“禁止圆角”作为本次讨论的目标,也不将上轮消费者的局部形状修正自动升格为系统规则。此前形状二选一的问题不再作为本轮待决入口。
+
+复核根级 DESIGN.md、L3 定位与执行记录及用户提供的 Why & What 草案后,当前判断为:指南对角色选择、配色配对、内容适应、组件组合和维护责任已有可用说明,但没有充分建立从共同设计立场到视觉表达、页面组合与取舍边界的联系。L3 首版主要归档了 L1/L2 已认可结论,知识发现与包内交付通过,不代表它已经能够指导陌生页面延续 InKCre 的视觉风格。Why & What 主要解释设计决策和上下文关系,不能独自承担具体视觉风格的定义。
+
+需要区分已认可的设计理念、现有实现中的风格线索和尚未确认的提议。不能把旧代码的所有表现都总结成品牌规范,也不能只增加“简洁、克制”等宽泛形容词。后续讨论应以少量认可的真实界面与对照方案,明确内容与操作的主次、文字和留白的节奏、颜色与边界的用途、容器的组织方式,以及变化时应保持的关系;再据此修订正式指南和消费验收。
+
+本轮仅审视并更新工作包,没有修改正式 DESIGN.md、Token、组件或消费者源码。已有下游迁移修正及检查仍有效,但不据此宣称设计风格已获认可;文档不足也不能解释所有执行遗漏,现有指南中已明确的要求仍须落实。
+
+## F0 知识拆分实施
+
+用户认可按设计立场、视觉语言、页面组合、判断依据四类知识继续处理,并明确当前问题是侧重点不对、单文件容易形成 monolith。已从最新 origin/main 的 `3de2a66` 建立 `feat/design-knowledge`,保留上一轮工作包修改。本轮拥有 DESIGN.md 与四份设计正文、已有指南的责任归位、Agent 读取路径和同版本随包交付;不改变 Token、组件运行时或下游页面,不提交或发布尚待审阅的视觉主张。
+
+DESIGN.md 保持共同立场和按任务导航;四份正文位于 `docs/design/`,分别拥有原则、视觉、组合与判断证据。可跨平台的设计知识与 Web 具体调用分开;原有 CSS/主题契约回到样式指南,扩展/兼容操作回到维护入口,避免只是拆开一个仍需全文加载的大文件。生成与包检查沿用现有脚本,不引入文档框架。
+
+已有设计关系可以直接整理;现行表现中的视觉线索必须标为提炼稿,待认可的代表界面用于后续校准。已向用户询问代表界面的名称或链接,同时继续独立的责任梳理和可审阅正文。原有 B5 截图只用于说明已验证的特定关系,不作为完整页面品牌认证。
+
+已完成的人工正文为 [DESIGN.md](../../DESIGN.md) 的 18 行总纲,以及[设计立场](../../docs/design/principles.md)、[视觉语言](../../docs/design/visual-language.md)、[页面组合](../../docs/design/composition.md)、[判断依据](../../docs/design/reference.md)。正文围绕内容、注意、对象归属、动作后果与适应关系组织,解释圆角、边界和容器应如何参与这些关系。视觉语言区分既有角色约定与当前组件的表达线索;判断依据为两张既有 B5 图片注明可推广关系、验证情境和不代表完整页面风格的范围。
+
+消费者从总纲进入与当前页面有关的正文,再查组件参考;维护者从 AGENTS 进入同一设计源,结合 Token 或组件的现有维护指南处理变更。字体调用、主题传播、数据模型与操作授权留在各自技术入口。生成器同步总纲、四份正文和两张图片共 7 个文件;包检查验证完整文件集合、字节与链接。图片已纳入明确的 Git 跟踪范围,避免新 checkout 丢失文档资源。已补 [patch Changeset](../../.changeset/better-trees-begin.md),没有新增依赖或文档框架。
+
+验证结果:完整 `pnpm check` 通过,包含格式、lint、生成一致性、Intent、Token 工作流、类型、构建、真实 tarball 的严格独立安装与文档链接,以及 21 个 Story/120 个 Variant 的构建。对包内副本依次做删除正文、篡改图片、增加过期正文的临时实验,`skill:generate:check` 均按预期失败,随后完整恢复并运行基线。另核对 17 个设计源链接与锚点,以及 AGENTS/docs/index/.gitignore 的共享托管块,均保持有效;`git diff --check` 通过。这些证据验证交付完整性,不构成新的视觉认可。
+
+当前待校准的是代表页面中的整体强调程度、连续内容与独立容器的比例、几何与层次组合,以及阅读区和操作区的密度。用户尚未补充代表页面,本轮将这些判断保留为明确的提炼稿;未把既有消费者修正继续扩散成新规则。分支 `feat/design-knowledge` 保留可审阅改动,未提交、推送、发布或修改下游源码。
+
+## F0 正文清理与视觉语言确认
+
+用户指出,描述现有组件表现、提炼过程和待确认状态的元描述不应进入正式文档,可以保留在 task packet;视觉语言应分别、交互地确认。本轮按此边界清理 DESIGN.md、设计正文及 README/Skill 入口。正式文档保留设计选择、理由、具体例子与适用范围;当前工作进度、实现观察和候选主张只在本节记录。上节“将提炼稿保留在正文”的方式已被此次纠正替代。
+
+从视觉语言移入的实现观察:
+
+> 现有 Button、Input、Header、Switch 和 Popup 呈现出几条共同线索:以中性色表面承载内容,用文字、对齐和可辨认的边界组织界面,把明显色彩用于动作与状态。Header 品牌文字和技术标识使用等宽表达,连续阅读使用系统 UI 字体。多数常用控件呈平面的矩形轮廓,层次主要来自分隔、背景关系和位置。
+
+这些是对现有实现的观察,不作为认可的整体风格。此前据此提出的“让信息和操作直接可辨,让视觉秩序来自排版、空间与边界的配合”也不能仅靠改成规定口吻就进入正式指南。品牌文字的等宽表达、控件矩形轮廓、圆角与阴影的组合等,后续分别讨论。既有文字角色、配色配对、字段关系与内容增长约定继续保留。
+
+判断依据中的两张图片分别来自 B5 的 `form-production-320.png` 与 `states-dark.png`。前者为 1000px 画布内的 320px 表单内容区域,含双语与重复校验文案;后者是多种状态的并列验证阵列。它们只用于解释字段关联与动作状态,不作为完整页面样板。正文改为直接解释这些设计关系和例子的适用范围,移除 B5 验收过程与品牌认可进度。
+
+确认按以下顺序展开,每次只处理一项,用户也可以调整顺序。每项确认后将选择及理由写入对应正文,再继续下一项;不把一次性提供全部代表页面作为前置条件。
+
+1. 整体气质:界面给人的感受与视觉性格。
+2. 文字表达:标题与正文的对比、等宽表达的使用范围。
+3. 色彩表达:中性色基底、强调色的强度与用途、浅深主题的关系。
+4. 形状与层次:边界、圆角、底色和阴影如何共同区分控件与容器。
+5. 空间与密度:阅读节奏、操作密度和不同容器中的变化。
+
+第一项候选建议为“平静、有秩序,略带技术感”:内容先进入视线,排版和对齐建立节奏,控件的强调跟随任务;技术感来自清晰、精确的组织。此建议尚未获得用户确认,不写入正式视觉语言,也不预先决定等宽字体、圆角或色值。
+
+清理已完成并同步随包副本。`pnpm format:check`、`pnpm check:skills`、`pnpm check:package`、18 个设计源链接与锚点核对及 `git diff --check` 通过。沿用上一轮完整基线,按本轮正文及生成模板文案的影响复核格式、发现与实际安装包;未重复运行组件与 Story 检查。当前只等待第一项设计反馈,未提交或发布。
+
+第一项已确认:用户同意“平静、有秩序,略带技术感”,同时指出“内容先进入视线、排版与空间建立阅读节奏、控件可辨、强调随任务分配”属于基本 interface guideline,不能当作视觉风格的具体定义。正式视觉语言只补入已确认的气质句。后续判断必须区分可用性底线与审美选择:即使两个方案都清楚、好用,也要说明 InKCre 为什么在字体、形状、色彩和质感上选择其中一种。已有正文中的通用关系仍是基础约束,不能计作风格已定义;随具体风格确认再整理其合适归属。
+
+下一项讨论文字的视觉性格。候选建议:系统 UI 无衬线承担正文与普通控件,品牌名及少量短栏目名、编号使用等宽表达,以局部字体对比形成轻微技术感。等宽字的品牌用途尚未确认,不能因现有 Header 恰好如此就视为标准;不提前决定所有标题、字重、字号或引入字体依赖。
+
+已确认的气质句已同步包内副本,正文格式、`pnpm check:skills` 与差异检查通过;本次没有改变链接、交付机制或运行时,未重复整包构建。未提交或发布。
+
+第二项的字体组合已确认:用户认可“无衬线为主、局部等宽混排”,并指出现有 monospace 有过度使用,这个方向也用于纠正实现。已在视觉语言中写明普通正文/控件的默认字体、品牌/少量栏目名/编号的局部等宽表达,以及避免容器继承扩散的边界。用户同时要求参考中西文排印指南;通用排印规则与风格选择分开,不将外部作者的偏好自动作为 InKCre 标准。
+
+2026-09-13 查阅的排印依据:
+
+- [W3C 中文排版需求](https://www.w3.org/TR/clreq/) 涵盖中文标点、行首行尾禁则、中西混排、字距和行段布局;查阅版本为 2026-09-01 Group Note Draft,作为研究中的参考,不宣称是强制标准。后续提取适用于界面文字的规则,按简繁与地区差异处理,不整套照搬书籍版式。
+- [Butterick:Body text](https://practicaltypography.com/body-text.html) 将字体、字号、行距与行长联系起来,也承认 Web 正文可采用衬线或无衬线字体;本项目采用无衬线是自身的风格选择。
+- [Butterick:Line length](https://practicaltypography.com/line-length.html) 给出西文平均每行 45—90 字符的经验范围。它是西文排印建议,不转换成中文统一字数或全局宽度 Token。
+
+排印指南的具体采纳项仍待梳理确认,先覆盖标点与断行、中西文混排、正文行长和行距的配合。字体回退与宿主排版支持归技术实现,品牌字体对比归视觉语言,不新增排版框架、字体依赖或全局文本改写机制。
+
+只读盘点发现 `packages/web/src/components/inkSwitch/inkSwitch.scss:14` 对整个普通状态控件应用 mono,是后续收敛项;Header 品牌、JsonEditor 与显式 mono 的 Textarea 各有对应用途。迁移 worktree 中 Source 的 id-label 与 id 共用 mono,需区分说明标签与编号;JobCard 编号、ExtensionCard 版本号和配置代码不可仅凭出现 mono 就判为过度使用。client-webext 的若干容器也有 monospace,但它仍属此前记录的独立主题范围。当前没有修改这些实现,后续按确认后的设计统一复核,不能据此宣称下游字体迁移完成。
+
+下一项仍在文字表达内:标题与正文的视觉反差。候选方向是标题适度增大、以中等或半粗字重形成稳定层级,正文保持常规字重;不普遍采用极粗的大标题。具体字重与尺度尚未确认,不改变现有 Token。
+
+字体组合正文已格式化并同步包内副本,`pnpm check:skills` 和差异检查通过。当前改动为文档与工作包,未提交或发布。
+
+标题力度已确认:用户认为目前的标题/正文对比力度较好。正式视觉语言记录“适度的字号差异、中等或半粗标题、常规字重正文”,维持现有 Token 度量,不将此确认解释为重新调整字号或字重。文字风格已确认局部等宽混排与温和明确的标题层级;中西文排印规则的整理仍是独立的基础工作,未因风格确认而宣称完成。
+
+下一项为色彩整体表达,候选建议是以接近黑白的中性色构成大部分界面,主要通过明暗差形成视觉对比,彩色仅在少量重点与状态中出现。该项讨论色彩面积和整体观感,不替代已确认的语义配色与可读性要求,也不预先决定具体色值;尚待用户确认。
+
+标题力度正文已同步包内副本,格式、`pnpm check:skills` 和差异检查通过。本轮未改变 Token 或组件实现,未提交或发布。
+
+色彩方向已确认:用户认可界面框架以黑白灰为主、少量彩色,并补充彩色应尽量偏灰、偏淡、低饱和。正式视觉语言记录小面积、低饱和、带灰调的柔和表达。这里将“淡”理解为不鲜艳、不刺眼的整体感受,不将其直接解释成所有颜色提高明度或统一降低透明度;具体明度仍需结合背景,已有文字和状态的可读性约定继续有效。用户未确认新的调色板或具体色值,本轮不修改颜色 Token。
+
+后续具体色板重点确认灰调的程度与色彩冷暖,而不是要求用户裁决通用可读性要求。浅深背景下的明度与对比由实现验证,在两种主题中保持低饱和的整体感受;尚未确定具体色值或主题表面方案。
+
+色彩方向正文已格式化并同步包内副本,`pnpm check:skills` 与差异检查通过;未改变运行时、提交或发布。
+
+用户要求继续后,进入色调的可视比较。[交互色调对照](color-study.html) 与[截图](color-study.png) 展示 A 中性灰、B 偏冷灰、C 偏暖灰的浅深两组样本。布局、文案、字体和三个灰调彩色保持相同,改变中性表面、文字和边界的冷暖,先判断框架底色的偏好。示意布局只用于控制比较条件,不把其控件形状、间距或字号升格为新的认可规则。建议优先考虑 A,以接近黑白的框架承载局部灰调彩色;用户尚未选择,正式视觉语言和 Token 不变。
+
+对照是无需服务或依赖的单文件 HTML,支持浅深并看/只看浅色/只看深色。通过现有下游 Playwright 在 Chromium 中检查 6 个样本、视图切换、390px 窄容器无横向溢出及无页面脚本错误,并查看截图;18 组反馈色与背景配对的计算对比度均不低于 4.5:1。浏览器已关闭,没有留下开发服务器。HTML 与截图属于 task packet 实验资产,不进入随包设计正文;未提交或发布。
+
+用户选择 A 中性灰方向。正式视觉语言补入“基础灰保持中性,不整体偏向蓝灰或米灰”,与先前的小面积低饱和灰调彩色组合。认可范围是基础色调的冷暖,不包含实验中的精确色值、字体度量、边界粗细、形状或布局;颜色 Token 未改。
+
+下一项讨论形状与层次,先确认整体轮廓:候选方向是平直、利落的几何,以矩形轮廓为主,局部用小圆角柔化转折,整体保持平面感。圆角的适用位置、层次所需的边界/底色/阴影仍分别判断,不将候选转成全局直角禁令或统一卡片样式。此候选尚待用户确认,不写入正式正文。
+
+中性灰方向已同步随包正文,格式、`pnpm check:skills` 与差异检查通过;未提交或发布。
+
+形态方向已确认并收紧:用户基本认可平直、利落、矩形为主及薄而平面的质感,指出“局部用小圆角柔化转折”过于宽泛。系统几乎都采用无圆角,任意加入圆角会突兀;卡片不能仅因是局部组件而获得圆角例外。本轮撤回这一宽泛候选,正式正文明确默认直角,卡片、按钮、输入框与浮层共享轮廓语言。例外须按明确形态用途约定,结合周围界面确认,不能以小面积或柔化装饰为理由自行引入。当前没有新增任何圆角例外,也不将这次收紧扩展成对所有圆形内容的禁止。
+
+后续需要区分“对象本身的形状”与“通用容器的装饰”:例如旋钮或环形进度的圆形是否属于必要的功能表达,可以单独讨论;它们不自动授权卡片、按钮和输入框采用圆角。具体例外尚待确认,候选留在工作包,不预先写入正式正文。Token 中已有圆角值也不构成设计许可,本轮不改 Token 或组件实现。
+
+形态正文已格式化并同步包内副本,`pnpm check:skills` 与差异检查通过;未提交或发布。
+
+用户确认上述圆角边界并要求继续。下一项为层次与阴影,候选表达是平整的版面、清晰的细实线边界与轻微底色差;页面及常驻内容区默认无阴影。实际覆盖其他内容的菜单、弹层若需要进一步表达前后关系,可以单独使用范围小、强度低、贴近轮廓的阴影。这个用途不能扩散成普通卡片的悬浮效果;阴影也不赋予圆角例外。此项尚待确认,只记录在工作包,不修改正式设计正文、Token 或组件。
+
+用户已确认层次与阴影方向,正式正文补入常驻内容默认无阴影、细实线与轻微底色差,以及临时覆盖层的小范围低强度阴影;继续遵循直角轮廓。本轮不修改阴影 Token 或组件实现。
+
+下一项为视觉空间与密度,候选方向是整体适度紧凑、局部舒展:列表和工具区具有稳定的行列节奏,相关信息距离较近;连续阅读区以段落与区块间的留白形成停顿。较大的留白集中在内容分段和页面边缘,不给每个字段或条目都加宽阔的内边距。该项讨论可辨识的空间节奏,不是固定像素表,也不以缩小字体或压缩操作目标实现紧凑;尚待用户确认,不写入正式正文。
+
+层次与阴影正文已格式化并同步随包副本,`pnpm check:skills` 与差异检查通过;未提交或发布。
+
+空间与密度已确认,正式视觉语言记录整体适度紧凑、局部舒展,较大的留白集中在段落、区块和页面边缘。用户同时指出,Agent 容易填满界面,而留白之前应先减少内容、元素;这种减少涉及交互状态、内容层级和多种尺寸,不能简化为静态页面的删减。本轮将这一判断写入设计立场,页面组合展开删除重复/无价值元素、按需进入次要详情、在正确状态呈现必要反馈及跨尺寸保留上下文,判断依据补充来源列表/详情/编辑的例子。既有“完整阅读不截断”的要求继续有效;减少的是当前视图负担,不是丢失必要内容。
+
+五项视觉方向已完成本轮确认,随即归整正文责任。视觉语言集中表达已确认的气质、字体对比、标题力度、中性灰与灰调彩色、默认直角、平面层次及空间节奏;文字角色表、配色角色与状态边界回到现有 Web 样式指南,内容取舍与分层由页面组合拥有。中西文排印参考进入判断依据,简要整理混排间距、语言与地区标点、行距与行长共同判断,强调显示调整不改写用户原文和代码;没有采用固定中文字数、全局字距算法或新依赖。
+
+尚未完成的是颜色和阴影等具体值的正式实验、现有 monospace 等实现偏差的收敛、下游按更新设计知识的复审与完整页面验证。当前文档收拢不等于这些实现已迁移,也不新增圆角例外;本轮继续只改设计与技术指南及其随包副本。
+
+收拢后的视觉语言为 33 行,按五项已确认方向组织;通用呈现原则、具体角色调用、排印参考各有明确入口。已重新生成随包正文,完整 `pnpm check` 通过,包含生成一致性、Intent、Token 工作流、类型、构建、真实 tarball 的独立安装与文档链接,以及 21 个 Story/120 个 Variant 构建。另核对 27 个本地设计/样式链接、共享托管块与差异,均通过。没有改变组件源码、Token JSON、锁文件或共享文档;分支仍保留可审阅改动,未提交、推送或发布。
+
+## F0/G3 已确认风格的实现对齐
+
+用户要求继续,按已确认规则从文档进入实现。先完成有明确判断依据的局部修正,颜色和阴影的具体值仍需场景对照;内容取舍另按列表、详情、编辑、反馈与尺寸的状态关系处理,不以统一隐藏详情代替设计。
+
+| 切片 | 目标与拥有范围 | 当前状态与验收 |
+| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| I1 字体与轮廓 | Switch 普通状态使用无衬线;AutoForm 根错误容器保持直角;AutoForm、Image Story 延续同一风格并提供真实可操作示例;下游 Twitter 步骤编号采用共同轮廓 | 已验证;生产者与下游完整检查、实际产物及 Story 浏览器验证通过,另修复窄屏向导关闭按钮裁剪;见 [I1 证据](i1-evidence/README.md) |
+| I2 色彩与阴影 | 对照基础色、动作和反馈的浅深主题配对,校准临时覆盖层阴影;具体值确认后再改 Token 和实现 | 已确认、正式落地并验证;Token、生成输出、指南、示例与 Changeset 已同步,完整检查和[实际产物验收](i2-evidence/README.md)通过;已提交 `284ade6` 并推送至 PR #46,未发布 |
+| I3 页面内容与状态 | 以真实 Sources 列表/详情/配置编辑等场景审视重复信息、按需详情和必要反馈,并覆盖窄宽容器与状态变化 | 已提交并推送至 PR #104;真实 preview 页面、本机 SSH 数据库 E2E 与最终 CI 均通过,未合入 main |
+
+I1 从普通状态等宽、错误面板圆角及示例自行装饰,收敛到已确认的无衬线默认与直角轮廓。拥有两个组件 SCSS、相关 Story/说明、适用 Changeset 和现有消费者的步骤样式;不改变模型、事件、schema 校验、业务权限、保存边界或 Token 值。Switch 切换与 pending 保持稳定宽度,错误仍直接可见,Image 的键盘打开/关闭和槽内操作应继续可用。没有新增依赖、测试框架或通用样式重置。
+
+复核修正了一处先前的盘点假设:Source 的 `id-label` 实际只显示 `#`,与编号共同使用等宽具有明确用途。Job 编号、版本、日志时间/结构化值、配置代码和技术类型标识也不因使用 mono 就需要删除。`client-webext` 仍是独立主题,不混入这批直接 UI 消费者修正。下游执行在既有 `feat/ui-v2-migration` worktree,由其工作包记录验证。
+
+I1 已完成。Image 示例去除没有行为的 Download/Share 按钮,改为实际原图下载链接;缩略图提示持续可见,替代文字与实际标志图片一致。AutoForm 调试数据继续用局部等宽,颜色随主题变化。生产者只修改两个组件的样式和相关示例/说明,未改模型、事件或 Token JSON;已补 patch Changeset。
+
+下游窄屏复验发现并修复了同一向导的基础宽度问题:88vw 没有扣除宿主弹层内边距,造成关闭按钮被裁剪。通过容器最大宽度约束解决,夹具增加内容区与操作区的完整可见性检查。这属于本次真实页面验收发现的必要修正,不恢复 Firefox 原生文字放大专项。
+
+最终两仓 `pnpm check` 均通过。生产者候选 tarball 验证中英文 Switch 的尺寸、切换、pending、外层 mono 隔离,以及浅深主题、320px/768px 下的错误呈现;Story 验证实际键盘与下载行为;消费者重放真实 Host/Twitter/Mail,保持既有交互通过。详见 [I1 验收](i1-evidence/README.md)。分支保留可审阅修改,未提交、推送或发布;消费者仍安装正式 2.0.0。
+
+下一切片 I2 以常用动作、反馈与临时覆盖层做浅深主题对照,先决定配对关系和视觉强度,再更新数值。I3 独立处理真实页面中的内容取舍与状态关系,避免借调色或轮廓修改掩盖信息层级问题。
+
+用户现已授权提交,并要求 I1 独立提交后继续 I2。F0 设计知识与交付已先归档为 `1e9a4a5`,本次 I1 的组件样式、Story、Changeset 与验收证据另作独立提交;下游同一切片也在迁移 worktree 单独归档。沿用上一轮最终完整检查与浏览器结果,本轮没有额外修改运行时;不将本地提交等同推送或发布。
+
+## I2 色彩与阴影:场景校准
+
+前置提交已完成:生产者 I1 为 `11be433`,消费者 I1 为 `6a61637`,两个工作区均已归档干净后开始此切片。用户本轮的 l1/l2 按紧邻上下文中的 I1/I2 执行,不回到已经发布的历史 L1/L2 整治。
+
+**当前状态:I2 正式落地与实际产物验证已完成。** 选定值沿现有 Token 角色维护;颜色和阴影属于可继续迭代的设计参数,不再要求逐个色值确认。下文保留各次实验与讨论的历史边界,最终结果见本节末尾与 [I2 验收](i2-evidence/README.md)。
+
+本轮从偏蓝灰主动作、较鲜艳的反馈、深色背景的大幅明暗跳变及较宽的覆盖层阴影,校准为中性主动作、灰调彩色、小幅表面层次与贴近边缘的阴影。先在 task packet 中给出真实组件的现状/候选对照;继续沿用逐项视觉确认约定,候选值不直接进入正式 Token。拥有本切片的实验、配对记录和后续明确的迁移路径,不新增主题引擎、色彩依赖或 Token 角色。
+
+验证覆盖浅深主题下的正文与次要说明、primary/subtle/danger 的普通/hover/pressed/focus/pending/disabled 状态、错误字段的边框和焦点共存、反馈在普通表面上的前景配对,以及 Tooltip 的实际覆盖关系。按影响进行窄容器与真实组件复核;不把通过对比度计算视为视觉偏好已确认,也不把实验页面当作真实下游迁移完成。
+
+已完成 [I2 交互对照](i2-study/index.html) 与[实施/验证记录](i2-study/README.md)。候选按现有角色覆盖,实际组件来自 I1 tarball:主动作和 Switch 收敛为中性灰,反馈采用灰绿/灰赭/灰蓝/灰红,深色底面与内容区的关系由较大明暗跳变改为小幅提亮。危险动作在深色主题同时调整前景和背景;阴影采用中性黑,并缩小偏移与模糊范围。精确值集中于实验文件,不进入正式规范或生成物。
+
+两种主题、现行/候选四组共检查 168 个配对;候选 84 组均达到本次文字 4.5:1、必要边界 3:1 的要求,最低分别为约 4.78:1 和 3.61:1。现行深色有 10 个不足组合,集中在较亮 hover 表面上的次要文字、部分反馈和必要边界,记录为角色配对缺口,不据此宣称具体页面全部失败。实际组件状态、错误与焦点共存、pending、Tooltip 开关以及三个视图的 375px 布局均通过;没有页面脚本错误或 HTTP(S) 请求。
+
+实验为离线单文件 HTML,复用现有构建与浏览器工具,没有新增生产依赖或测试框架。本轮只改工作包及实验,I1 后的组件、Token JSON、生成物、包版本与消费者源码保持原状。已将具体视觉力度交给用户继续校准;下一步依据反馈修订或将选定配对正式落地,而不是直接把实验值视为已认可默认。
+
+用户本轮认可整体色彩,特别认可危险动作按钮的红色,同时指出两项修正:真实页面应避免密集使用成功/警告/信息色;需要引起注意的语义与文字应使用较纯的颜色,上一版绿、黄、蓝过于雾化。由此修正先前“所有彩色都低饱和”的概括:中性框架保持安静,危险动作底色保留已认可灰红;需要强调的反馈前景按用途提高纯度,常态状态与过程说明不自动着色。错误文字也按这一用途与危险按钮底色分开校准,不把对按钮红色的认可推导成所有红色前景都必须同值。
+
+本轮拥有范围是视觉语言/页面组合的已确认规则、对应 Web 样式与 Agent 配方、两个普通提交反馈示例,以及现有 I2 实验和验证。实验移除默认页上的成功/警告堆叠,四种语义反馈通过当前情境切换,详细说明保持中性色。中性表面、动作底色、Switch 和阴影候选不变;新反馈前景仍待修订稿的视觉校准。正式 Token、组件运行时和下游消费者不在本次调整中改变,I3 的真实页面内容复审仍独立执行。
+
+修订版已验证:候选 84 个配对全部通过,浅/深主题最低文字对比约为 5.14:1/4.95:1,最低必要边界约为 3.86:1/3.61:1;实际按钮状态、错误与焦点共存、Tooltip 和 375px 布局继续通过。新增 20 组情境核对,确认普通状态用中性色、需要注意的反馈只在关键文字着色,说明正文及普通保存/等待结果保持中性色。已查看六张更新截图,并分别查看绿、蓝、红前景在浅深背景上的实际文字。浏览器日志为本机 `/tmp/inkcre-i2-feedback-verify.log`,可复跑脚本与结果保存在实验目录。
+
+正式指南、Agent 配方与两个普通提交示例已同步生成,补充 patch Changeset;先前 F0 的待发布摘要同时改为“中性灰与少量彩色重点”,避免继续泛化低饱和要求。完整 `pnpm check` 通过,包含实际包安装、配方/文档交付和 21 个 Story/120 个 Variant;日志为本机 `/tmp/inkcre-i2-feedback-check.log`。最后只补入“默认”一词澄清中性色规则的适用边界,重新生成并检查 Skill。已请用户继续校准修订前景的纯度,未将数值检查当作审美确认。没有提交、推送或发布,下游 worktree 保持干净。
+
+用户已确认修订色彩,I2 随即进入正式源。150 个 Token 路径与类型保留;品牌色族映射到中性色阶,危险动作底色与错误前景按用途分开维护。选定的浅深配对、Switch 与三个阴影档位同步到 Sass/CSS/Uno,没有新增角色、依赖或主题引擎。指南说明两种阴影入口的覆盖差异,迁移文档区分未发布默认主题与已发布 2.0.0 契约;既有 I2 patch Changeset 已更新。
+
+完整 `pnpm check` 通过。换色揭示循环引用夹具依赖旧正文色的隐式关系,现改为显式构造闭环,不修改生成器或导入规则。实际 tarball 在根级主题下与冻结样稿逐项比较,浅深各 41 个相关变量一致;按钮全状态、错误与焦点、选中标签/说明、Tooltip、Teleport 浮层和 375px 操作均通过。Sass/Uno 三档可见阴影一致,并实际验证运行时变量只影响 Sass/Tooltip 的既有边界。六张截图已查看,无页面脚本错误或网络请求;产物身份、哈希和可复跑记录见 [I2 验收](i2-evidence/README.md)。
+
+I2 已验证并按用户授权独立提交为 `284ade6`,未推送或发布。消费者仍使用正式 registry 2.0.0,发布后的安装复验尚未执行。下一次视觉调整按已维护的用途与配对修改 Token,再检查受影响状态即可,不重新展开整轮风格确认,也不恢复 Firefox 原生 200% 文字放大专项。
+
+## I3 页面内容与状态
+
+用户授权提交 I2 后开始 I3,本轮 l3 按紧邻的 I1—I3 切片执行。下游继续使用 `client-web-ui-v2-migration` 隔离 worktree;生产者仅更新本工作包。具体实现、验证与产物身份由消费者 `tasks/ui-v2-migration/packet.md` 和现有 evidence 目录记录。
+
+源码追踪确认 SourceCard 仅在 Sources 列表使用,列表常驻创建表单、配置 JSON 和行内昵称编辑,详情同时再提供整份修改表单。列表与删除读取还存在将数据库错误当作空数据/成功的问题;详情的来源类型只在初次 setup 读取,来源返回前得到空结果,后续未重新获取。
+
+本轮将列表收敛为名称、类型、ID、当前采集入口,创建表单由明确动作打开;完整配置与删除集中到有对象上下文的详情。详情保留配置修改,调度创建按需进入,现有计划与任务直接呈现。拥有下游 Source 列表/详情/表单与必要模型错误边界、局部 Job 状态表达、双语文案、说明、Changeset 和现有浏览器验收。保留 Core 数据与采集语义、JSON 原始草稿、失败恢复、窄宽操作、注册表依赖及 MF 共享;不新增主题覆盖或通用状态框架。以完整检查及真实构建的页面旅程验证,网络夹具只证明前端状态与请求,不替代真实数据库或 OAuth 集成。
+
+I3 已完成本轮来源工作流与关联消费修正。列表保留可比较的身份和采集入口,详情集中完整配置修改,现有任务与计划保持直接可见,创建表单按操作进入。读取失败不再当作空结果或无限加载,写入等待与拒绝保留草稿;返回列表定位原来源,过期响应不能覆盖新路由,新建任务使用已保存来源的类型规则。
+
+真实依赖追踪补齐 Twitter 来源切换失败的旧选择/时间草稿保护。截图还发现 Host 漏注册 UI Uno preset,使错误类没有生成,以及窄屏创建弹层过窄;已在消费者的相应入口修复。没有增加依赖、主题覆盖或通用状态层,也没有改变本仓库 Token/组件实现。
+
+消费者最终完整 `pnpm check`、TS7、Changeset 状态及实际 Host/Twitter/Mail 重放通过,pageerror 为零,1280px/375px 代表截图已查看。数据库 E2E 本次仍因 SSH 握手连接重置未进入浏览器;额外 type-aware lint 的 11 项既有问题没有混入此切片。范围、截图、产物哈希与受控读写证据见消费者 `tasks/ui-v2-migration/evidence/README.md` 的 I3 节,不沿用旧 PR CI 证明新改动。
+
+生产者 I2 为 `284ade6`;本工作包的新记录与消费者 I3 尚未提交,未推送或发布。消费者继续锁定 registry UI 2.0.0,因此 I3 截图不包含尚未发布的 I2 颜色。I1—I3 的约定实施切片已分别落实;这不代表全部消费者页面审视已经完成,UI 更新发布后的实际安装复验和 client-webext 独立主题仍各自保留后续边界。
+
+## PR preview 交付
+
+用户授权提交、推送与创建 PR,使用 preview 环境完成验收。生产者保留 F0、I1、I2 独立提交,新增本工作包记录后提交 UI PR;消费者沿用 PR #104,提交 I3 并在其独立 worktree 完成远端验证。GitHub 与数据库主机的 SSH 远端命令已成功,真实数据库 E2E 正在复跑。生产者 preview 验证当前 Histoire,消费者 preview 验证 Host 与同提交的 MF 产物;消费者仍安装 registry UI 2.0.0,两个证据不能互相替代。
+
+远端 UI preview 发现两项展示构建错误:Histoire 的状态同步读取 Vue app `_instance.proxy` 时实例为空;选中 Variant 后代码区调用 `codeToHtml` 失败。构建配置把组件 Vue 切到 CDN 的生产版本,同时伪造空的 Shiki highlighter,与 Histoire 自带运行时契约冲突。此次验收修复拥有 `histoire.config.ts` 与展示文档/工作包,撤除这两套自定义替换,使用已锁定的 Histoire/Vite 常规打包。保留组件、Token、包公开入口和发布责任,以本地正式 Story 构建及远端相同操作复验,不增加兼容补丁或运行时 fallback。
+
+展示构建修复后的完整 `pnpm check` 与本地正式产物浏览器复验通过:浅深主题各 41 个变量和 I2 冻结证据一致,Switch 键盘/宽度/pending、Image 下载/焦点、原生提交及 Histoire Variant 控件/源码面板均正常,pageerror 为零。恢复常规依赖打包后 vendor 约 12.24 MB(gzip 2.31 MB),不再在运行时借 CDN 隐藏依赖体积;展示站后续若需性能优化,应使用兼容 Histoire 的分包或高亮配置,而非伪造运行时。
+
+## 本轮最终交付状态
+
+UI PR #46 和消费者 PR #104 均已提交并推送。生产者设计知识、I1、I2 与展示运行时修复保留独立提交;消费者 I1、I3 与数据库冷启动修复亦独立归档。生产者 `9128425` 的 CI 与 preview 全部成功,实际远端复验通过且 pageerror 为零,见 [preview 证据](preview-evidence/README.md)。消费者最终文档提交 `0f2ed00` 的 CI 与 preview 全部成功;I3 实际远端 Host/Registry/Mail/Twitter 全旅程及本机 SSH 五个真实数据库旅程通过。
+
+本轮没有合并 PR、生产部署或发布新包/扩展。消费者继续使用正式 UI 2.0.0;新配色发布后的真实安装复验、client-webext 独立主题以及总图中未执行组仍保留原边界,不以本轮通过宣称全部消费者迁移完成。
+
+## PR #104 图标缺失诊断
+
+用户要求确认菜单/侧栏切换图标是否因 UI Uno safelist 丢失。本轮限定调查与浏览器隔离探针,没有修改组件、消费者源码或依赖。真实 preview 的 Menu 按钮具有 24×24 尺寸,`.i-mdi-menu` 规则与 SVG mask 均已生成;更具体的 `button.ink-header__menu-icon` 将背景覆盖成透明,使 mask 没有可见填色。在当前 DOM 仅把背景恢复为 currentColor,菜单图形立即可见,点击仍正常打开侧栏。根因归属 UI 的 `inkHeader.scss`,不是 safelist。
+
+正式 registry 2.0.0 与生产者当前构建均包含九个组件内置图标选择器。沿既有真实 Host/Mail/Twitter/来源旅程核对当前可见图标,菜单是唯一具有 mask 却填色透明的项;关闭、刷新、下拉/展开箭头与加载图标在已覆盖状态下正常。不能据此宣称所有可能图标和状态都已覆盖。此前浏览器验收检查交互与 pageerror,未验证菜单的实际绘制,这一缺口需要在修复时补上。
+
+建议在 UI Header 内分离 button 与装饰图标 span,沿用 InkButton 的结构:按钮负责透明背景、焦点和命中区域,子元素负责 mask 与 currentColor;保留 aria-label、menu-click 和按钮尺寸。不要在消费者添加重复 safelist 或永久覆盖。发布修复包后更新消费者并验证;当前 PR #104 仍安装 2.0.0,仅修改 UI PR 不会让它自动得到修复。
+
+用户已授权修复、提交和推送。实现从按钮自身承载 mask 改为子 span 绘制图标,按钮保留透明背景、24×24 最小命中区域及独立焦点轮廓。拥有 Header Vue/SCSS、现有 Basic Story/说明、生成 Skill、patch Changeset 和任务证据;保留 props、事件、right-icon 槽、Token 及消费者依赖。通过完整 check、正式构建的浅深/窄宽浏览器检查验证图标填色、键盘焦点与逐次事件,不添加消费者 safelist 或覆盖。
+
+Header 修复已通过完整 `pnpm check` 和正式 Story 产物的浅深/375px/1280px 检查,图标填色、未被 mask 裁剪的按钮焦点及三种输入的事件次数均正确,pageerror 为零。已补 patch Changeset;详见 [Header 验收](header-evidence/README.md)。本轮不发布新包、不改消费者依赖,随后推送 UI PR #46 进行远端复验。
+
+截图复核发现首次拆分后,子图标按 12px 字体尺寸绘制,改变了原按钮 mask 的 24px 图形尺寸。改用已有 apply-icon(md, true) 的图标盒尺寸,使独立图标保持原来的 24px;验收同时检查图形和按钮尺寸,避免仅验证命中区域而漏掉视觉缩小。
+
+保留 24px 图形尺寸后,最终完整 `pnpm check` 与四组浏览器检查再次通过;浅深菜单截图已复核。该收尾只使用原有图标尺寸 helper,没有改变 Token、事件或消费者样式。
+
+Header 修复已提交推送:`6b988c3` 分离按钮与图标,`d6125cf` 保留原有 24px 图形尺寸;最终源码的完整 CI 和 preview 均通过。真实远端浅深/375px/1280px 的图形、填色、焦点及三种输入事件全部通过,pageerror 为零,证据与部署身份见 [Header 验收](header-evidence/README.md)。消费者诊断记录已单独提交 `6644122` 并通过 CI;其依赖仍为 2.0.0,本轮没有发布或安装修复版本。
diff --git a/tasks/ui-foundations/preview-evidence/README.md b/tasks/ui-foundations/preview-evidence/README.md
new file mode 100644
index 0000000..15131e2
--- /dev/null
+++ b/tasks/ui-foundations/preview-evidence/README.md
@@ -0,0 +1,19 @@
+# PR preview 验收
+
+[UI PR #46](https://github.com/InKCre/ui/pull/46) 的 `9128425` 通过[完整 CI](https://github.com/InKCre/ui/actions/runs/34759790243) 和 [Histoire preview 交付](https://github.com/InKCre/ui/actions/runs/34759846429)。本轮实际进入[远端 preview](https://pr-46.design-dd4.pages.dev),验证正式构建的控件与展示应用;源提交和不可变部署 URL 见[结果](verification.json)。
+
+浅深主题各 41 个变量和 I2 冻结值一致。375px 下验证 Switch 键盘切换、宽度稳定、系统 UI 字体和 pending;Image 键盘打开、Escape、焦点归还和实际 SVG 下载;Button 的原生表单提交。1280px 下通过 Histoire 正常导航选择 Variant、修改状态并查看源码,pageerror 为零。
+
+- Switch:[浅色](switch-light-375.png)、[深色](switch-dark-375.png)。
+- Image 下载:[浅色](image-light-375.png)、[深色](image-dark-375.png)。
+- [展示控件与源码面板](story-source.png)。截图已人工复核;Histoire 外壳的绿色、圆角属于展示工具,不作为 InKCre 消费页面的视觉范本。
+
+复跑现有任务脚本,第二个参数使用已有 Playwright 的 `index.mjs` 绝对路径,不增加本仓库依赖:
+
+```sh
+pnpm exec node tasks/ui-foundations/preview-evidence/verify.mjs /absolute/path/to/@playwright/test/index.mjs https://pr-46.design-dd4.pages.dev
+```
+
+远端首次复验发现 Histoire 原配置将 Vue 替换为 CDN 生产版本,并用空对象模拟 Shiki,造成状态同步和源码面板错误。独立 `9128425` 删除这两层替换,完整 `pnpm check` 与本地正式产物检查通过后,再完成本次远端复验。当前展示站常规打包的 vendor 约 12.24 MB(gzip 2.31 MB),保留后续性能优化的真实边界。
+
+消费者仍安装 registry UI 2.0.0,不包含本 PR 尚未发布的 I2 配色。消费者的 Host/Mail/Twitter 实际 preview 与 SSH 数据库结果由其 PR #104 工作包拥有,不能用此处的展示页替代下游安装验收。
diff --git a/tasks/ui-foundations/preview-evidence/image-dark-375.png b/tasks/ui-foundations/preview-evidence/image-dark-375.png
new file mode 100644
index 0000000..d744d09
Binary files /dev/null and b/tasks/ui-foundations/preview-evidence/image-dark-375.png differ
diff --git a/tasks/ui-foundations/preview-evidence/image-light-375.png b/tasks/ui-foundations/preview-evidence/image-light-375.png
new file mode 100644
index 0000000..cd59c07
Binary files /dev/null and b/tasks/ui-foundations/preview-evidence/image-light-375.png differ
diff --git a/tasks/ui-foundations/preview-evidence/story-source.png b/tasks/ui-foundations/preview-evidence/story-source.png
new file mode 100644
index 0000000..173e9a6
Binary files /dev/null and b/tasks/ui-foundations/preview-evidence/story-source.png differ
diff --git a/tasks/ui-foundations/preview-evidence/switch-dark-375.png b/tasks/ui-foundations/preview-evidence/switch-dark-375.png
new file mode 100644
index 0000000..e66409d
Binary files /dev/null and b/tasks/ui-foundations/preview-evidence/switch-dark-375.png differ
diff --git a/tasks/ui-foundations/preview-evidence/switch-light-375.png b/tasks/ui-foundations/preview-evidence/switch-light-375.png
new file mode 100644
index 0000000..b0ae76e
Binary files /dev/null and b/tasks/ui-foundations/preview-evidence/switch-light-375.png differ
diff --git a/tasks/ui-foundations/preview-evidence/verification.json b/tasks/ui-foundations/preview-evidence/verification.json
new file mode 100644
index 0000000..be89ac6
--- /dev/null
+++ b/tasks/ui-foundations/preview-evidence/verification.json
@@ -0,0 +1,28 @@
+{
+ "origin": "https://pr-46.design-dd4.pages.dev",
+ "browser": "149.0.7827.55",
+ "checks": [
+ {
+ "theme": "light",
+ "matchedVariables": 41,
+ "switch": "keyboard, width, sans, pending",
+ "image": "keyboard, Escape, focus return, real SVG download",
+ "button": "native submit"
+ },
+ {
+ "theme": "dark",
+ "matchedVariables": 41,
+ "switch": "keyboard, width, sans, pending",
+ "image": "keyboard, Escape, focus return, real SVG download",
+ "button": "native submit"
+ },
+ {
+ "histoire": "variant controls, reactive update and rendered source"
+ }
+ ],
+ "errors": [],
+ "source": "9128425dd26023c3e09981124758d7e23e6e7007",
+ "deployment": "https://45e22f48.design-dd4.pages.dev",
+ "workflow": "https://github.com/InKCre/ui/actions/runs/34759846429",
+ "checks_workflow": "https://github.com/InKCre/ui/actions/runs/34759790243"
+}
diff --git a/tasks/ui-foundations/preview-evidence/verify.mjs b/tasks/ui-foundations/preview-evidence/verify.mjs
new file mode 100644
index 0000000..b288918
--- /dev/null
+++ b/tasks/ui-foundations/preview-evidence/verify.mjs
@@ -0,0 +1,101 @@
+// 本任务的远端 Histoire 复验,复用已安装的 Playwright。
+import { pathToFileURL } from "node:url";
+import { readFile, writeFile } from "node:fs/promises";
+const { chromium, expect } = await import(pathToFileURL(process.argv[2]).href);
+const origin = process.argv[3];
+const expected = JSON.parse(
+ await readFile(new URL("../i2-evidence/verification.json", import.meta.url)),
+);
+const browser = await chromium.launch();
+const page = await browser.newPage({ viewport: { width: 375, height: 800 } });
+const errors = [];
+page.on("pageerror", (e) => errors.push(e.message));
+const report = { origin, browser: browser.version(), checks: [], errors };
+const output = (name) => new URL(name, import.meta.url).pathname;
+async function story(id, variant, theme) {
+ await page.goto(`${origin}/__sandbox.html?storyId=${id}&variantId=${id}-${variant}`);
+ await page.locator("button, input, .story-debug").first().waitFor();
+ await page.evaluate(
+ (theme) => document.documentElement.classList.toggle("dark", theme === "dark"),
+ theme,
+ );
+ await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
+}
+try {
+ for (const theme of ["light", "dark"]) {
+ await story("stories-controls-inkswitch-story-vue", 6, theme);
+ const values = await page.locator("html").evaluate((root) => {
+ const style = getComputedStyle(root),
+ probe = document.createElement("span");
+ root.append(probe);
+ const values = Object.fromEntries(
+ Array.from(style)
+ .filter(
+ (name) =>
+ name.startsWith("--sys-color-") ||
+ name.startsWith("--comp-switch-") ||
+ name.startsWith("--sys-elevation-raised-"),
+ )
+ .sort()
+ .map((name) => {
+ let value = style.getPropertyValue(name).trim();
+ if (CSS.supports("color", value)) {
+ probe.style.color = value;
+ value = getComputedStyle(probe).color;
+ }
+ return [name, value];
+ }),
+ );
+ probe.remove();
+ return values;
+ });
+ expect(values).toEqual(expected.themes.find((t) => t.theme === theme).tokens);
+ const toggle = page.getByRole("switch", { name: "通知开关", exact: true });
+ const before = await toggle.boundingBox();
+ await toggle.press("Space");
+ await expect(toggle).toHaveAttribute("aria-checked", "true");
+ expect((await toggle.boundingBox()).width).toBe(before.width);
+ await expect(toggle).toHaveCSS("font-family", "system-ui, -apple-system, sans-serif");
+ await expect(page.getByRole("switch", { name: "等待中的通知开关" })).toBeDisabled();
+ await page.screenshot({ path: output(`switch-${theme}-375.png`) });
+ await story("stories-media-inkimage-story-vue", 3, theme);
+ const thumbnail = page.getByRole("button", { name: "InKCre 标志", exact: true });
+ await thumbnail.press("Enter");
+ await expect(page.getByRole("dialog")).toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(thumbnail).toBeFocused();
+ await story("stories-media-inkimage-story-vue", 5, theme);
+ await page.getByRole("button", { name: "InKCre 标志", exact: true }).click();
+ const download = page.waitForEvent("download");
+ await page.getByRole("link", { name: "下载原图(SVG)" }).click();
+ expect((await download).suggestedFilename()).toBe("inkcre.svg");
+ await expect(page.getByRole("dialog")).toBeVisible();
+ await page.screenshot({ path: output(`image-${theme}-375.png`) });
+ await story("stories-controls-inkbutton-story-vue", 13, theme);
+ await page.getByRole("button", { name: "普通动作", exact: true }).click();
+ await expect(page.getByText("提交次数:0")).toBeVisible();
+ await page.getByRole("button", { name: "提交", exact: true }).click();
+ await expect(page.getByText("提交次数:1")).toBeVisible();
+ report.checks.push({
+ theme,
+ matchedVariables: Object.keys(values).length,
+ switch: "keyboard, width, sans, pending",
+ image: "keyboard, Escape, focus return, real SVG download",
+ button: "native submit",
+ });
+ }
+ await page.setViewportSize({ width: 1280, height: 900 });
+ await page.goto(origin);
+ await page.getByText("Controls", { exact: true }).click();
+ await page.getByRole("link", { name: /^Switch/ }).click();
+ await page.getByRole("link", { name: "长状态文案保持轨道尺寸", exact: true }).click();
+ await expect(page.locator("pre").filter({ hasText: "v-model" }).first()).toBeVisible();
+ await page.getByRole("switch", { name: "通知开关", exact: true }).click();
+ await page.screenshot({ path: output("story-source.png") });
+ report.checks.push({ histoire: "variant controls, reactive update and rendered source" });
+ expect(errors).toEqual([]);
+ await writeFile(output("verification.json"), JSON.stringify(report, null, 2) + "\n");
+ console.log(JSON.stringify(report));
+} finally {
+ await browser.close();
+}
diff --git a/tokens/inkcre.tokens.json b/tokens/inkcre.tokens.json
index eff2953..409f8e5 100644
--- a/tokens/inkcre.tokens.json
+++ b/tokens/inkcre.tokens.json
@@ -3,38 +3,38 @@
"elevation": {
"raised": {
"low": {
- "description": null,
+ "description": "临时覆盖层的轮廓阴影;常驻内容默认不使用。",
"type": "custom-shadow",
"value": {
"shadowType": "dropShadow",
"radius": 2,
- "color": "#0c153014",
+ "color": "#0000001a",
"offsetX": 0,
"offsetY": 1,
"spread": 0
}
},
"md": {
- "description": null,
+ "description": "临时覆盖层的轮廓阴影;常驻内容默认不使用。",
"type": "custom-shadow",
"value": {
"shadowType": "dropShadow",
- "radius": 20,
- "color": "#0c15301f",
+ "radius": 6,
+ "color": "#00000029",
"offsetX": 0,
- "offsetY": 6,
+ "offsetY": 2,
"spread": 0
}
},
"high": {
- "description": null,
+ "description": "临时覆盖层的轮廓阴影;常驻内容默认不使用。",
"type": "custom-shadow",
"value": {
"shadowType": "dropShadow",
- "radius": 32,
- "color": "#0c155429",
+ "radius": 12,
+ "color": "#0000002e",
"offsetX": 0,
- "offsetY": 12,
+ "offsetY": 4,
"spread": 0
}
}
@@ -145,7 +145,7 @@
},
"40": {
"type": "color",
- "value": "#b9192fff",
+ "value": "#b3342bff",
"blendMode": "normal"
},
"60": {
@@ -160,7 +160,7 @@
},
"70": {
"type": "color",
- "value": "#ff8889ff",
+ "value": "#ed9690ff",
"blendMode": "normal"
},
"80": {
@@ -182,67 +182,67 @@
"brand": {
"2": {
"type": "color",
- "value": "#0b1220ff",
+ "value": "{ref.color.neutral.2}",
"blendMode": "normal"
},
"6": {
"type": "color",
- "value": "#121928ff",
+ "value": "{ref.color.neutral.6}",
"blendMode": "normal"
},
"12": {
"type": "color",
- "value": "#1f2635ff",
+ "value": "{ref.color.neutral.12}",
"blendMode": "normal"
},
"20": {
"type": "color",
- "value": "#303747ff",
+ "value": "{ref.color.neutral.20}",
"blendMode": "normal"
},
"30": {
"type": "color",
- "value": "#474e5fff",
+ "value": "{ref.color.neutral.30}",
"blendMode": "normal"
},
"40": {
"type": "color",
- "value": "#5f6677ff",
+ "value": "{ref.color.neutral.40}",
"blendMode": "normal"
},
"50": {
"type": "color",
- "value": "#787e91ff",
+ "value": "{ref.color.neutral.50}",
"blendMode": "normal"
},
"60": {
"type": "color",
- "value": "#9198acff",
+ "value": "{ref.color.neutral.60}",
"blendMode": "normal"
},
"70": {
"type": "color",
- "value": "#acb3c7ff",
+ "value": "{ref.color.neutral.70}",
"blendMode": "normal"
},
"80": {
"type": "color",
- "value": "#b2bccfff",
+ "value": "{ref.color.neutral.80}",
"blendMode": "normal"
},
"90": {
"type": "color",
- "value": "#c8cee3ff",
+ "value": "{ref.color.neutral.90}",
"blendMode": "normal"
},
"94": {
"type": "color",
- "value": "#f7f7fbff",
+ "value": "{ref.color.neutral.94}",
"blendMode": "normal"
},
"98": {
"type": "color",
- "value": "#fbfcffff",
+ "value": "{ref.color.neutral.98}",
"blendMode": "normal"
}
}
@@ -395,7 +395,7 @@
"base": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.2}"
+ "value": "{ref.color.neutral.12}"
},
"subtle": {
"description": "普通表面上的辅助文字;不用于 primary 或 danger 表面。",
@@ -410,7 +410,7 @@
"on-danger": {
"description": "与 surface.danger 及其 hover 配对的反色前景。",
"type": "color",
- "value": "{ref.color.danger.98}"
+ "value": "{ref.color.neutral.98}"
}
},
"surface": {
@@ -427,12 +427,12 @@
"primary": {
"description": "",
"type": "color",
- "value": "{ref.color.brand.6}"
+ "value": "{ref.color.brand.12}"
},
"primary-hover": {
"description": "",
"type": "color",
- "value": "{ref.color.brand.30}"
+ "value": "{ref.color.brand.20}"
},
"subtle-hover": {
"description": "",
@@ -440,14 +440,14 @@
"value": "{ref.color.neutral.94}"
},
"danger": {
- "description": "",
+ "description": "危险动作底色,与 text.on-danger 成对使用;不从反馈前景推导。",
"type": "color",
- "value": "{ref.color.danger.35}"
+ "value": "#825955"
},
"danger-hover": {
- "description": "",
+ "description": "危险动作的 hover/pressed 底色,保持 text.on-danger 可读。",
"type": "color",
- "value": "{ref.color.danger.30}"
+ "value": "#704947"
},
"base-hover": {
"description": "",
@@ -474,24 +474,24 @@
},
"feedback": {
"error": {
- "description": "",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。",
"type": "color",
"value": "{ref.color.danger.40}"
},
"success": {
"type": "color",
- "value": "#176b39",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#19713d",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
},
"warning": {
"type": "color",
- "value": "#7a4b00",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#8a5900",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
},
"info": {
"type": "color",
- "value": "#005c8a",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#1267a7",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
}
},
"overlay": {
@@ -509,34 +509,34 @@
"base": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.98}"
+ "value": "{ref.color.neutral.90}"
},
"subtle": {
"description": "普通表面上的辅助文字;不用于 primary 或 danger 表面。",
"type": "color",
- "value": "{ref.color.neutral.80}"
+ "value": "{ref.color.neutral.70}"
},
"on-primary": {
"description": "与 surface.primary 及其 hover 配对的反色前景。",
"type": "color",
- "value": "{ref.color.brand.2}"
+ "value": "{ref.color.brand.12}"
},
"on-danger": {
"description": "与 surface.danger 及其 hover 配对的反色前景。",
"type": "color",
- "value": "{ref.color.danger.98}"
+ "value": "{ref.color.neutral.12}"
}
},
"surface": {
"base": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.30}"
+ "value": "{ref.color.neutral.12}"
},
"subtle": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.12}"
+ "value": "{ref.color.neutral.20}"
},
"primary": {
"description": "",
@@ -546,34 +546,34 @@
"primary-hover": {
"description": "",
"type": "color",
- "value": "{ref.color.brand.60}"
+ "value": "{ref.color.brand.80}"
},
"subtle-hover": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.40}"
+ "value": "#3a3a3a"
},
"danger": {
- "description": "",
+ "description": "危险动作底色,与 text.on-danger 成对使用;不从反馈前景推导。",
"type": "color",
- "value": "{ref.color.danger.40}"
+ "value": "#c8a19d"
},
"danger-hover": {
- "description": "",
+ "description": "危险动作的 hover/pressed 底色,保持 text.on-danger 可读。",
"type": "color",
- "value": "{ref.color.danger.35}"
+ "value": "#b9918b"
},
"base-hover": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.40}"
+ "value": "{ref.color.neutral.20}"
}
},
"border": {
"base": {
"description": "必要的控件边界;装饰分隔使用 border.subtle。",
"type": "color",
- "value": "{ref.color.neutral.70}"
+ "value": "{ref.color.neutral.60}"
},
"strong": {
"description": "",
@@ -588,24 +588,24 @@
},
"feedback": {
"error": {
- "description": "",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。",
"type": "color",
- "value": "{ref.color.danger.80}"
+ "value": "{ref.color.danger.70}"
},
"success": {
"type": "color",
- "value": "#75d69a",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#67c88c",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
},
"warning": {
"type": "color",
- "value": "#ffd27a",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#e5b951",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
},
"info": {
"type": "color",
- "value": "#8bceff",
- "description": "用于 base/subtle 表面的状态文字;同时提供明确状态文案。"
+ "value": "#70b5ed",
+ "description": "普通表面上需要注意的反馈前景;配合明确文案,例行状态默认使用中性色。"
}
},
"overlay": {
@@ -624,7 +624,7 @@
"track-bg": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.2}"
+ "value": "{ref.color.neutral.12}"
},
"handle-bg": {
"description": "",
@@ -634,7 +634,7 @@
"label-color": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.2}"
+ "value": "{ref.color.neutral.12}"
}
}
},
@@ -643,17 +643,17 @@
"track-bg": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.98}"
+ "value": "{ref.color.neutral.90}"
},
"handle-bg": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.2}"
+ "value": "{ref.color.neutral.12}"
},
"label-color": {
"description": "",
"type": "color",
- "value": "{ref.color.neutral.98}"
+ "value": "{ref.color.neutral.90}"
}
}
}
diff --git a/tokens/tokens.md b/tokens/tokens.md
index 8c33b59..bc21fc3 100644
--- a/tokens/tokens.md
+++ b/tokens/tokens.md
@@ -2,22 +2,24 @@
[inkcre.tokens.json](inkcre.tokens.json) 是共享设计规则的规范源,由仓库维护角色、用途说明、类型和引用。当前保留 Figma 导出使用的 `type`、`value` 字段及 `custom-shadow` 类型,并非使用 `$type`、`$value` 的标准 DTCG 输入。Figma 可以提议已有路径的值更新;新增、删除、改名、类型和引用变化必须在仓库显式迁移,导入不能覆盖整份文件。
-| 源层 | 责任 | Web 输出 |
-| --- | --- | --- |
-| `ref` | 基础色值、字体家族、空间、尺寸、圆角、断点、透明度 | `_ref.scss` 分类映射 |
-| `typography` | 文本角色的字号、比例行高、字重、字距 | `$font` 和 `--sys-font-*` |
-| `effect.elevation` | 阴影结构及透明颜色 | `$elevation` |
-| `sys.light/dark.color` | 文本、表面、边界、反馈和遮罩的主题用途 | `$color-light`、`$color-dark` |
-| `comp.light/dark` | Switch 的轨道、滑块和文字配色 | `$light`、`$dark` |
+| 源层 | 责任 | Web 输出 |
+| ---------------------- | -------------------------------------------------- | ----------------------------- |
+| `ref` | 基础色值、字体家族、空间、尺寸、圆角、断点、透明度 | `_ref.scss` 分类映射 |
+| `typography` | 文本角色的字号、比例行高、字重、字距 | `$font` 和 `--sys-font-*` |
+| `effect.elevation` | 阴影结构及透明颜色 | `$elevation` |
+| `sys.light/dark.color` | 文本、表面、边界、反馈和遮罩的主题用途 | `$color-light`、`$color-dark` |
+| `comp.light/dark` | Switch 的轨道、滑块和文字配色 | `$light`、`$dark` |
非颜色系统映射 `$base` 由生成器引用 ref 的分类和文本角色构成,不是遍历任意 sys 节点。源格式与角色可供将来的平台实现读取;目前只交付 Web 输出,没有 Flutter 或 uniapp 渲染器。
## 角色与度量维护
-共同的角色选择、配色关系、内容增长和扩展判断见 [DESIGN.md](../DESIGN.md)。本文件说明源格式与维护入口,不另裁决设计规则。
+共同立场从 [DESIGN.md](../DESIGN.md) 进入;文字、配色与空间关系由[视觉语言](../docs/design/visual-language.md) 维护,共享与变化理由由[设计立场](../docs/design/principles.md) 维护。本文件说明源格式与维护入口,不另裁决设计规则。
文本角色由字号、比例行高、字重和字距构成;精确值和叶节点用途在规范源中维护。字号采用 rem,行高为无量纲比例。不要把某个根字号下的像素结果反写成所有环境的固定要求。字体家族和装饰独立于尺寸角色。
-修改名称、用途、值或引用时,核对 DESIGN.md 中跨角色的关系、受影响的组件与覆盖能力。新增或改变公开语义需要迁移说明;Figma 值导入不能改变这些元数据。
+修改名称、用途、值或引用时,核对相应设计正文中跨角色的关系、受影响的组件与覆盖能力。新增或改变公开语义需要迁移说明;Figma 值导入不能改变这些元数据。
+
+颜色调整从 `sys.light/dark.color` 的用途进入,连同前景、背景和交互状态一起维护。`ref.color.brand` 引用中性色阶;`ref.color.danger` 提供红色基础值,不决定危险按钮的底色。`surface.danger` 与 `feedback.error` 分别维护,避免调整按钮面积色时连带改变需要注意的错误文字。不要为了把所有色值塞入同一色阶而建立缺少共同变化理由的引用。
生成和导入的检查见[生成说明](../scripts/build-tokens.md);默认消费方式、运行时覆盖范围见[样式指南](../packages/web/styles/README.md)。源变更须同时检查这些承诺及实际组件表现,生成成功不是视觉验收。