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 表单内容区域中的长标签和错误说明自然换行](examples/form-content.png) + +在 320px 宽的表单内容区域中,长标签和错误说明换行,字段随内容增长。每条说明紧邻对应字段,红色前景表达错误,标题、正文与标签保留各自的阅读职责。若为维持固定字段高度而裁切文字,用户就可能失去修正输入所需的信息。 + +这个例子展示字段内部的关联。截图外侧留白不参与表单布局;实际页面的外部空间与字段密度按容器和内容安排。 + +## 操作与状态的区别 + +![深色主题中的普通动作、主要动作、危险动作与反馈](examples/action-states.png) + +主要动作、辅助动作和危险动作有不同表达;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 表单内容区域中的长标签和错误说明自然换行](examples/form-content.png) + +在 320px 宽的表单内容区域中,长标签和错误说明换行,字段随内容增长。每条说明紧邻对应字段,红色前景表达错误,标题、正文与标签保留各自的阅读职责。若为维持固定字段高度而裁切文字,用户就可能失去修正输入所需的信息。 + +这个例子展示字段内部的关联。截图外侧留白不参与表单布局;实际页面的外部空间与字段密度按容器和内容安排。 + +## 操作与状态的区别 + +![深色主题中的普通动作、主要动作、危险动作与反馈](examples/action-states.png) + +主要动作、辅助动作和危险动作有不同表达;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 (/]*type=["']importmap["'][^>]*>/i.test(html)) { - htmlTransformed = true; - return; - } - - // Create import map with proper JSON serialization - const importMap = { - imports: Object.fromEntries( - Object.entries(cdnModules).map(([name, url]) => [name, url]), - ), - }; - const importMapScript = ` \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/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 @@ + + + + + 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 @@ + + + + + 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)。源变更须同时检查这些承诺及实际组件表现,生成成功不是视觉验收。