iter(g185): 分割线统一改走细分边框令牌

This commit is contained in:
HIsheR
2026-10-26 17:45:43 +08:00
parent ecd24d55be
commit 999f3f6ec1
9 changed files with 88 additions and 41 deletions
+1 -1
View File
@@ -49,7 +49,7 @@
--ua-leading-normal: 1.55; /* 正文档:多行正文、说明文字 */
/* ---- 边框:细分(比基础边框更淡,用于分割线,避免 1px 线过重) ---- */
--ua-border-subtle: color-mix(in srgb, var(--ua-color-border) 60%, transparent); /* 细分边框色:表格分割线、卡片内分隔 */
--ua-border-subtle: color-mix(in srgb, var(--ua-color-border) 60%, transparent); /* 细分边框色(专用于分割线):表格行线 / 卡片页脚 / 弹窗抽屉页头页脚 / 列表项 / 选项卡底边 / 折叠面板与步骤条连接线 / 分隔线组件 */
/* ---- 阴影:四档 elevation,柔和优先(扩散大、透明度低) ---- */
--ua-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06); /* 1 档:贴地投影,用于卡片 / 输入组 / 轻浮起 */
+19 -19
View File
@@ -102,7 +102,7 @@
gap: var(--ua-space-1);
margin-top: var(--ua-space-3);
padding-top: var(--ua-space-2);
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
.ua-card-borderless {
border: none;
@@ -121,7 +121,7 @@
.ua-table th,
.ua-table td {
padding: var(--ua-space-table-cell);
border-bottom: 1px solid var(--ua-color-border);
border-bottom: 1px solid var(--ua-border-subtle);
text-align: left;
font-size: var(--ua-font-size-md);
white-space: nowrap;
@@ -134,7 +134,7 @@
background: var(--ua-color-surface);
font-weight: var(--ua-weight-medium);
color: var(--ua-color-text-muted);
box-shadow: inset 0 -1px 0 var(--ua-color-border);
box-shadow: inset 0 -1px 0 var(--ua-border-subtle);
}
.ua-table th.ua-sortable {
cursor: pointer;
@@ -439,7 +439,7 @@ body.ua-col-resizing {
}
.ua-option-group:not(:first-child) {
margin-top: var(--ua-space-1);
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
padding-top: var(--ua-space-2);
}
.ua-option {
@@ -563,7 +563,7 @@ body.ua-col-resizing {
.ua-menu-divider {
height: 1px;
margin: var(--ua-space-1) var(--ua-space-05);
background: var(--ua-color-border);
background: var(--ua-border-subtle);
}
/* —— 面包屑 —— */
@@ -666,7 +666,7 @@ body.ua-col-resizing {
background: var(--ua-color-bg);
}
.ua-acc-item + .ua-acc-item {
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
.ua-acc-head {
display: flex;
@@ -976,7 +976,7 @@ body.ua-col-resizing {
top: 30px;
bottom: 2px;
width: 1px;
background: var(--ua-color-border);
background: var(--ua-border-subtle);
}
.ua-stepper.is-vertical .ua-step:last-child::after {
display: none; /* 末步没有下一段可连 */
@@ -999,7 +999,7 @@ body.ua-col-resizing {
height: 1px;
margin-top: 13px;
margin-right: var(--ua-space-2);
background: var(--ua-color-border);
background: var(--ua-border-subtle);
flex-shrink: 0;
}
.ua-step-dot {
@@ -1140,12 +1140,12 @@ button.ua-step-dot:hover {
content: '';
flex: 1;
height: 1px;
background: var(--ua-color-border);
background: var(--ua-border-subtle);
}
.ua-divider-dashed::before,
.ua-divider-dashed::after {
background: none;
border-top: 1px dashed var(--ua-color-border);
border-top: 1px dashed var(--ua-border-subtle);
}
.ua-divider-label {
font-size: var(--ua-font-size-sm);
@@ -1165,7 +1165,7 @@ button.ua-step-dot:hover {
}
.ua-divider-vertical.ua-divider-dashed {
background: none;
border-left: 1px dashed var(--ua-color-border); /* 垂直虚线走边框 */
border-left: 1px dashed var(--ua-border-subtle); /* 垂直虚线走边框 */
}
/* —— 徽标 —— */
@@ -1573,7 +1573,7 @@ button.ua-step-dot:hover {
gap: var(--ua-space-1);
max-width: 100%;
overflow-x: auto;
border-bottom: 1px solid var(--ua-color-border);
border-bottom: 1px solid var(--ua-border-subtle);
}
.ua-tab {
padding: 7px var(--ua-space-35);
@@ -1820,7 +1820,7 @@ button.ua-step-dot:hover {
justify-content: flex-end;
gap: var(--ua-space-2);
padding: var(--ua-space-3) var(--ua-space-4);
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
/* —— 弹窗 —— */
@@ -1862,7 +1862,7 @@ button.ua-step-dot:hover {
justify-content: flex-end;
gap: var(--ua-space-2);
padding: var(--ua-space-3) var(--ua-space-4);
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
@keyframes ua-fade {
from {
@@ -1880,7 +1880,7 @@ button.ua-step-dot:hover {
align-items: center;
justify-content: space-between;
padding: var(--ua-space-3) var(--ua-space-4);
border-bottom: 1px solid var(--ua-color-border);
border-bottom: 1px solid var(--ua-border-subtle);
}
.ua-modal-title {
margin: 0;
@@ -2157,7 +2157,7 @@ button.ua-step-dot:hover {
top: 6px;
bottom: 0;
width: 1px;
background: var(--ua-color-border);
background: var(--ua-border-subtle);
}
.ua-tl-right::before {
left: auto;
@@ -2496,10 +2496,10 @@ button.ua-step-dot:hover {
.ua-list-header {
font-weight: var(--ua-weight-semibold);
color: var(--ua-color-text);
border-bottom: 1px solid var(--ua-color-border);
border-bottom: 1px solid var(--ua-border-subtle);
}
.ua-list-footer {
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
.ua-list-body {
display: flex;
@@ -2513,7 +2513,7 @@ button.ua-step-dot:hover {
transition: background var(--ua-duration-fast) ease;
}
.ua-list-item + .ua-list-item {
border-top: 1px solid var(--ua-color-border);
border-top: 1px solid var(--ua-border-subtle);
}
.ua-list-clickable {
cursor: pointer;
+1 -1
View File
@@ -62,7 +62,7 @@ function writeBaseline(root, result) {
const file = path.join(root, BASELINE_REL);
const total = KIND_META.reduce((sum, meta) => sum + (result.counts[meta.key] ?? 0), 0);
const data = {
$schema: '视觉漂移基线:五类硬编码视觉字面量的允许上限,任一类超出即 ua audit:visual 退出码 1;迁移后用 --baseline 重写',
$schema: '视觉漂移基线:各类硬编码视觉字面量的允许上限,任一类超出即 ua audit:visual 退出码 1;迁移后用 --baseline 重写',
file: UI_CSS_REL,
commit: shortCommit(root),
generatedAt: new Date().toISOString(),
+50 -4
View File
@@ -9,7 +9,7 @@
/*** 皮肤包样式表相对仓库根的路径(迁移与审计共同的目标文件) ***/
export const UI_CSS_REL = 'packages/skin/ui/ui.css';
/*** 作用:五类硬编码的键名、中文名与判定口径,审计输出与报告都按这个顺序 ***/
/*** 作用:各类硬编码的键名、中文名与判定口径,审计输出与报告都按这个顺序 ***/
export const KIND_META = [
{
key: 'font-size-literal',
@@ -61,6 +61,11 @@ export const KIND_META = [
label: '层级字面值',
hint: 'z-index 写死数字,应改用 --ua-z-* 五档(组件内 1/2 微排序豁免)',
},
{
key: 'border-literal',
label: '边框色字面值',
hint: 'border/border-color 写死色值(rgba()/#hex),组件轮廓用 --ua-color-border、分割线用 --ua-border-subtle',
},
];
/*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/
@@ -92,6 +97,30 @@ const SCAN_PROPS = new Set([
'font-weight',
'line-height',
'z-index',
'border',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-color',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]);
/*** 作用:边框类属性集合(轮廓与分割线共用一套色值口径,禁用字面色) ***/
const BORDER_PROPS = new Set([
'border',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-color',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]);
/*** 作用:内距类属性集合(迁移与统计共用同一判定) ***/
@@ -217,7 +246,7 @@ export function blankComments(src) {
/*** 作用:逐条产出「属性 + 值」及其在原文中的偏移;只认白名单属性且必须以分号收尾(避开伪类选择器) ***/
export function eachDeclaration(code) {
const re = /(padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|font-weight|line-height|z-index|min-height|max-height|height|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline)\s*:\s*([^;{}]*);/gi;
const re = /(border-top-color|border-right-color|border-bottom-color|border-left-color|border-color|border-top|border-right|border-bottom|border-left|border|padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|font-weight|line-height|z-index|min-height|max-height|height|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline)\s*:\s*([^;{}]*);/gi;
const list = [];
for (const m of code.matchAll(re)) {
const prop = m[1].toLowerCase();
@@ -350,6 +379,15 @@ function migrateZ(core) {
return { kind: 'z-literal', value: token };
}
/*** 作用:迁移单条边框:纯黑 rgba 边(历史上手写的「深一层黑边」)归到基础边框色;其余留在门禁里由人按轮廓/分割线语义定位 ***/
function migrateBorder(prop, core) {
if (!BORDER_PROPS.has(prop)) return null;
const value = squash(core);
const next = value.replace(/rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*[\d.]+\s*\)/g, 'var(--ua-color-border)'); // 空格宽容,避免只认一种写法
if (next === value) return null;
return { kind: 'border-literal', value: next };
}
/*** 作用:判定单条声明是否需要迁移,返回 { kind, value } 或 null(幂等:已是令牌就返回 null) ***/
export function migrateDeclaration(prop, rawValue) {
const { core, tail } = splitImportant(rawValue);
@@ -374,6 +412,8 @@ export function migrateDeclaration(prop, rawValue) {
result = migrateLeading(core);
} else if (prop === 'z-index') {
result = migrateZ(core);
} else {
result = migrateBorder(prop, core); // 边框类:只收敛纯黑 rgba 边
}
if (!result) return null;
return { kind: result.kind, value: result.value + tail };
@@ -441,10 +481,16 @@ function detect(prop, rawValue, key) {
if (value === '1' || value === '2') return 0; // 组件内微排序(表头压行、水印叠层、头像悬停),不是全局层级
return /^\d+$/.test(value) ? 1 : 0;
}
if (key === 'border-literal') {
if (!BORDER_PROPS.has(prop)) return 0;
if (value === 'none' || value === '0' || value === 'inherit') return 0; // 无边框与继承不算色值漂移
const stripped = value.replace(/var\(--ua-[\w-]+\)/g, ' '); // 已是令牌的色先剔除
return /(#[0-9a-f]{3,8}\b|rgba?\(|hsla?\()/i.test(stripped) ? 1 : 0;
}
return 0;
}
/*** 作用:扫描整段 CSS,返回五类硬编码的计数与命中行号(审计命令的唯一统计口径) ***/
/*** 作用:扫描整段 CSS,返回各类硬编码的计数与命中行号(审计命令的唯一统计口径) ***/
export function scanSource(src) {
const code = blankComments(src);
const lineAt = (offset) => {
@@ -487,7 +533,7 @@ export function migrateSource(src) {
return { text: out, counts };
}
/*** 作用:把五类计数拍平成「类别: 数量」数组,供审计报告与基线文件共用同一顺序 ***/
/*** 作用:把各类计数拍平成「类别: 数量」数组,供审计报告与基线文件共用同一顺序 ***/
export function countRows(counts) {
return KIND_META.map((meta) => ({ ...meta, count: counts[meta.key] ?? 0 }));
}
+3 -3
View File
@@ -19,7 +19,7 @@
"ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1",
"ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)",
"ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1",
"ua audit:visual [--json|--baseline]:视觉漂移门禁,统计皮肤包里五类硬编码视觉字面量(字号/动效时长/阴影/圆角/焦点环)并与 .ua/visual-baseline.json 棘轮比对,超基线退出码 1;--baseline 重写基线,--json 出机器可读计数",
"ua audit:visual [--json|--baseline]:视觉漂移门禁,统计皮肤包里各类硬编码视觉字面量(字号/动效时长/阴影/圆角/焦点环/内距/控件高度/字重/行高/层级/边框色)并与 .ua/visual-baseline.json 棘轮比对,超基线退出码 1;--baseline 重写基线,--json 出机器可读计数",
"ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
],
"keyTypes": [
@@ -39,9 +39,9 @@
"装好依赖后先跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)",
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤模糊找(「处理中」这类说明里的词也能搜到,命中非 id 标题时会标出命中字段;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
"查配方:ua recipes 列全量(编号 / 标题 / 用到的组件),ua recipes <编号> 看骨架代码与要点(--json 给脚本用;MCP 里对应 list_recipes / get_recipe)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十组:颜色 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 焦点环 / 控件高度 / 层级,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,焦点环用 --ua-focus-ring(_inset)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十一组:颜色 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 焦点环 / 控件高度 / 层级,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,焦点环用 --ua-focus-ring(_inset)",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"改完样式跑 npm run audit:visual:五类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌",
"改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
],
"components": []