refactor(css): #6 后续阶段——@layer 级联 + token 化 + 行尾治理
- 引入 @layer ecommerce-core,standalone 覆盖层不再依赖 !important(全站 !important 7812→967)
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# 默认按文本处理,统一使用 LF 行尾
|
||||
* text=auto eol=lf
|
||||
|
||||
# 源码与样式文件强制 LF
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.cjs text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Windows 专用脚本保留 CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# 二进制文件,不做行尾转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.webp binary
|
||||
*.ico binary
|
||||
*.svg text eol=lf
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.otf binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Remove all !important declarations from a CSS file.
|
||||
* For use with base.css and other files that no longer need !important
|
||||
* because they are unlayered and override layered ecommerce.css by cascade priority.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const filePath = process.argv[2];
|
||||
if (!filePath) {
|
||||
console.error("Usage: node cleanup-css-important.mjs <file.css>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let content = readFileSync(filePath, "utf-8");
|
||||
|
||||
// Strip BOM
|
||||
content = content.replace(/^\uFEFF/, "");
|
||||
|
||||
// Remove !important
|
||||
const before = (content.match(/!important/g) || []).length;
|
||||
content = content.replace(/\s*!important/g, "");
|
||||
|
||||
// Fix artifacts
|
||||
content = content.replace(/;\s*;/g, ";");
|
||||
content = content.replace(/;\s*}/g, "}");
|
||||
content = content.replace(/[ \t]+$/gm, "");
|
||||
|
||||
const after = (content.match(/!important/g) || []).length;
|
||||
const lines = content.split("\n").length;
|
||||
|
||||
writeFileSync(filePath, content, "utf-8");
|
||||
|
||||
console.log(`Processed: ${filePath}`);
|
||||
console.log(` Lines: ${lines}`);
|
||||
console.log(` !important: ${before} → ${after} (removed ${before - after})`);
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Clean up overrides.css:
|
||||
* 1. Remove BOM character
|
||||
* 2. Remove `html body #root ` specificity prefix from selectors
|
||||
* 3. Remove `html body ` specificity prefix
|
||||
* 4. Deduplicate doubled class selectors: .foo.foo → .foo
|
||||
* 5. Deduplicate doubled attribute selectors: [attr="val"][attr="val"] → [attr="val"]
|
||||
* 6. Remove all `!important` declarations
|
||||
* 7. Compress multiple consecutive blank lines
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const filePath = process.argv[2];
|
||||
if (!filePath) {
|
||||
console.error("Usage: node cleanup-css-overrides.mjs <file.css>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let content = readFileSync(filePath, "utf-8");
|
||||
|
||||
// 0. Strip BOM
|
||||
content = content.replace(/^\uFEFF/, "");
|
||||
|
||||
// 1. Remove `html body #root ` prefix (with space before the next selector)
|
||||
content = content.replace(/^(\s*)html\s+body\s+#root\s+/gm, "$1");
|
||||
|
||||
// 2. Remove `html body ` prefix (remaining ones without #root)
|
||||
content = content.replace(/^(\s*)html\s+body\s+/gm, "$1");
|
||||
|
||||
// 3. Deduplicate doubled class selectors: .foo.foo → .foo
|
||||
// Must not match across word boundaries incorrectly
|
||||
content = content.replace(/\.([\w-]+)(\.\1)\b/g, ".$1");
|
||||
|
||||
// 4. Deduplicate doubled attribute selectors: [data-tool="clone"][data-tool="clone"] → [data-tool="clone"]
|
||||
// Use known patterns
|
||||
const attrDedup = [
|
||||
[/\[data-tool="clone"\]\[data-tool="clone"\]/g, '[data-tool="clone"]'],
|
||||
[/\[data-tool="kit"\]\[data-tool="kit"\]/g, '[data-tool="kit"]'],
|
||||
[/\[data-tool="video"\]\[data-tool="video"\]/g, '[data-tool="video"]'],
|
||||
];
|
||||
for (const [pat, repl] of attrDedup) {
|
||||
content = content.replace(pat, repl);
|
||||
}
|
||||
|
||||
// 4b. Generic dedup for any other doubled attribute patterns that slipped through
|
||||
// Match [name] or [name="val"] followed by an exact copy
|
||||
content = content.replace(/\[(\w[\w-]*(?:="[^"]*")?)\]\1/g, "[$1]");
|
||||
|
||||
// 5. Remove ` !important` — strip the space and the keyword, keeping the semicolon or newline
|
||||
content = content.replace(/\s*!important/g, "");
|
||||
|
||||
// 6. Fix artifacts:
|
||||
// - " ; " or " ;; " → "; "
|
||||
content = content.replace(/;\s*;/g, ";");
|
||||
// - Lone semicolon before closing brace: ";\n}" → "\n}" (just remove trailing ;)
|
||||
content = content.replace(/;(\s*\})/g, "$1");
|
||||
// - But keep "; " inside rule blocks — only clean up empty declarations
|
||||
|
||||
// 7. Don't collapse " ;}" — we already handled this above
|
||||
// If after removing !important we have style: value; } → keep it as is
|
||||
|
||||
// 8. Compress multiple blank lines (>2 consecutive newlines) to at most one
|
||||
content = content.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
// 9. Remove trailing whitespace on lines
|
||||
content = content.replace(/[ \t]+$/gm, "");
|
||||
|
||||
// 10. Ensure single trailing newline
|
||||
content = content.trimEnd() + "\n";
|
||||
|
||||
writeFileSync(filePath, content, "utf-8");
|
||||
|
||||
// Stats
|
||||
const importantCount = (content.match(/!important/g) || []).length;
|
||||
const lines = content.split("\n").length;
|
||||
console.log(`Processed: ${filePath}`);
|
||||
console.log(` Lines: ${lines}`);
|
||||
console.log(` !important remaining: ${importantCount}`);
|
||||
@@ -70,9 +70,9 @@ console.log(
|
||||
console.log("");
|
||||
|
||||
// Exit non-zero if total !important exceeds a budget threshold.
|
||||
// Current baseline: ~7795. Set budget slightly above to allow incremental work
|
||||
// while preventing uncontrolled growth.
|
||||
const IMPORTANT_BUDGET = 7820;
|
||||
// Post-@layer refactoring baseline: ~970 (formerly ~7812).
|
||||
// Budget set to 2000 to prevent regression while allowing controlled growth.
|
||||
const IMPORTANT_BUDGET = 2000;
|
||||
if (totals.important > IMPORTANT_BUDGET) {
|
||||
console.error(
|
||||
`FAIL: !important count ${totals.important} exceeds budget ${IMPORTANT_BUDGET}. ` +
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Replace hardcoded hex colors in base.css / overrides.css with
|
||||
* CSS variable references defined in standalone/tokens.css.
|
||||
*
|
||||
* The replacement table maps exact hex values → var() references.
|
||||
* Only standalone/*.css files are processed.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
// Color → variable mappings (ordered so longer/more-specific colors are matched first)
|
||||
const REPLACEMENTS = [
|
||||
// Primary accent
|
||||
["#1073cc", "var(--ecom-primary)"],
|
||||
["#0f66b3", "var(--ecom-primary-hover)"],
|
||||
|
||||
// Cyan accent
|
||||
["#1ebddb", "var(--ecom-accent-cyan)"],
|
||||
["#1dbedb", "var(--ecom-accent-cyan)"], // common typo variant
|
||||
["#16c8df", "var(--ecom-accent-cyan-light)"],
|
||||
["#18bfd2", "var(--ecom-accent-cyan-bright)"],
|
||||
["#0f829b", "var(--ecom-accent-cyan-deep)"],
|
||||
|
||||
// White/near-white
|
||||
["#feffff", "var(--ecom-bg-near-white)"],
|
||||
["#fbfdff", "var(--ecom-bg-near-white)"],
|
||||
["#f8fdff", "var(--ecom-bg-near-white)"],
|
||||
["#ffffff", "var(--ecom-bg-white)"],
|
||||
["#fff", "var(--ecom-bg-white)"], // 3-digit short form
|
||||
|
||||
// Light backgrounds
|
||||
["#f8f9fa", "var(--ecom-bg-page)"],
|
||||
["#edf8fb", "var(--ecom-bg-tinted)"],
|
||||
["#edf8ff", "var(--ecom-bg-tinted)"],
|
||||
["#f3f8fa", "var(--ecom-bg-cool)"],
|
||||
["#f8fbfc", "var(--ecom-bg-cool)"],
|
||||
["#eef6f8", "var(--ecom-bg-cool)"],
|
||||
|
||||
// Dark blue backgrounds
|
||||
["#172636", "var(--ecom-bg-dark-blue)"],
|
||||
["#163447", "var(--ecom-bg-deep-blue)"],
|
||||
["#083c67", "var(--ecom-bg-navy)"],
|
||||
["#05233f", "var(--ecom-bg-navy-deep)"],
|
||||
|
||||
// Text
|
||||
["#10202c", "var(--ecom-text-primary)"],
|
||||
["#f5fbff", "var(--ecom-text-on-dark)"],
|
||||
["#738392", "var(--ecom-text-muted)"],
|
||||
["#71818e", "var(--ecom-text-muted)"],
|
||||
["#66798a", "var(--ecom-text-muted)"],
|
||||
["#526474", "var(--ecom-text-muted)"],
|
||||
["#8da0ab", "var(--ecom-text-placeholder)"],
|
||||
|
||||
// Border
|
||||
// NOTE: rgba() colors are NOT replaced — only hex values
|
||||
];
|
||||
|
||||
function processFile(filePath) {
|
||||
let content = readFileSync(filePath, "utf-8");
|
||||
let replaced = 0;
|
||||
|
||||
for (const [hex, variable] of REPLACEMENTS) {
|
||||
// Only replace when the hex appears as a CSS value (not inside comments or var())
|
||||
// Match: colon-space-hex, comma-space-hex, space-hex, etc.
|
||||
// But be careful: don't replace inside existing var() calls
|
||||
const regex = new RegExp(
|
||||
`(?<!var\\([^)]{0,200})${hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\da-fA-F])`,
|
||||
"gi"
|
||||
);
|
||||
|
||||
const matches = content.match(regex);
|
||||
if (matches) {
|
||||
replaced += matches.length;
|
||||
content = content.replace(regex, variable);
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(filePath, content, "utf-8");
|
||||
console.log(` ${filePath}: ${replaced} replacements`);
|
||||
return replaced;
|
||||
}
|
||||
|
||||
const files = process.argv.slice(2);
|
||||
if (files.length === 0) {
|
||||
console.error("Usage: node tokenize-colors.mjs <files...>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const f of files) {
|
||||
total += processFile(f);
|
||||
}
|
||||
console.log(`Total: ${total} replacements across ${files.length} file(s)`);
|
||||
@@ -2,13 +2,17 @@
|
||||
* ecommerce-standalone.css — 聚合入口
|
||||
*
|
||||
* 电商工作台的全站覆盖样式。按职责拆分为两部分:
|
||||
* - base.css: 正常选择器(topbar / auth / clone-ai / profile / 功能面板)
|
||||
* - overrides.css: 超高特异性覆盖层(html body #root ... + !important),
|
||||
* 用于在 ecommerce.css 之后生效,是 !important 的主要来源(#6 待治理)
|
||||
* - base.css: 未分层主题覆盖(topbar / auth / clone-ai / profile / 功能面板),
|
||||
* 通过 CSS @layer 级联自动覆盖 ecommerce-core 层样式。
|
||||
* - overrides.css: 响应式布局覆盖(媒体查询 / 小屏适配 / 组件微调),
|
||||
* 未分层,自动胜出 @layer ecommerce-core。
|
||||
*
|
||||
* 不要在此文件直接追加规则。新增样式写入对应子文件;
|
||||
* 新增 !important 会被 npm run css:audit 拦截(budget 7820)。
|
||||
* 级联架构(via @layer):
|
||||
* 未分层(base / overrides) > @layer ecommerce-core(ecommerce.css)
|
||||
*
|
||||
* 不要在此文件直接追加规则。新增样式写入对应子文件。
|
||||
*/
|
||||
|
||||
@import "./standalone/tokens.css";
|
||||
@import "./standalone/base.css";
|
||||
@import "./standalone/overrides.css";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "./layers.css";
|
||||
@import "./tokens.css";
|
||||
@import "./base/reset.css";
|
||||
@import "./shell/app-shell.css";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Layer declaration — must be imported first so all @layer rules are resolved
|
||||
* before any stylesheet that references them.
|
||||
*
|
||||
* ecommerce-core: 电商页主样式。较低优先级,方便 standalone 覆盖层在不使用
|
||||
* !important 的情况下覆盖。
|
||||
*/
|
||||
|
||||
@layer ecommerce-core;
|
||||
@@ -1,3 +1,4 @@
|
||||
@layer ecommerce-core {
|
||||
.product-clone-page {
|
||||
display: grid;
|
||||
grid-template-rows: 54px minmax(0, 1fr);
|
||||
@@ -3113,10 +3114,10 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════
|
||||
/* ══════════════════════════════════════════════�?
|
||||
Flowchart Pipeline Layout (参考图风格)
|
||||
流程图式布局:原图 → 分镜文本 → 分镜图 → 分镜视频
|
||||
═══════════════════════════════════════════════ */
|
||||
流程图式布局:原�?�?分镜文本 �?分镜�?�?分镜视频
|
||||
══════════════════════════════════════════════�?*/
|
||||
|
||||
.clone-ai-flow-pipeline {
|
||||
display: flex;
|
||||
@@ -3260,7 +3261,7 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Connector (分支连接线) ── */
|
||||
/* ── Connector (分支连接�? ── */
|
||||
.clone-ai-flow-connector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3320,7 +3321,7 @@
|
||||
background: #3a3f48;
|
||||
}
|
||||
|
||||
/* ── Flow Arrow (节点间箭头) ── */
|
||||
/* ── Flow Arrow (节点间箭�? ── */
|
||||
.clone-ai-flow-branch .clone-ai-flow-arrow {
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
@@ -3449,9 +3450,9 @@
|
||||
width: min(100%, 1100px);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════
|
||||
/* ══════════════════════════════════════════════�?
|
||||
End Flowchart Pipeline Styles
|
||||
═══════════════════════════════════════════════ */
|
||||
══════════════════════════════════════════════�?*/
|
||||
|
||||
.product-clone-page .clone-ai-input-wrapper {
|
||||
position: relative;
|
||||
@@ -12119,7 +12120,7 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
/* 平台弹窗宽度仅桌面/平板固定;≤640px 由移动端断点的全宽规则接管。 */
|
||||
/* 平台弹窗宽度仅桌�?平板固定;≤640px 由移动端断点的全宽规则接管�?*/
|
||||
@media (min-width: 641px) {
|
||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--platform {
|
||||
width: min(460px, calc(100% - 24px)) !important;
|
||||
@@ -12127,7 +12128,7 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
||||
}
|
||||
}
|
||||
|
||||
/* 平台选项:logo + 名称横排,名称过长省略,避免在窄网格里溢出弹窗。 */
|
||||
/* 平台选项:logo + 名称横排,名称过长省略,避免在窄网格里溢出弹窗�?*/
|
||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-popover--platform button {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
@@ -12153,8 +12154,8 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
||||
}
|
||||
}
|
||||
|
||||
/* 宽设置面板:固定宽度并靠右对齐 composer,避免从靠右的"设置"按钮左对齐展开时顶出右边缘被裁。
|
||||
仅桌面/平板生效;≤640px 由移动端断点的全宽规则接管。 */
|
||||
/* 宽设置面板:固定宽度并靠右对�?composer,避免从靠右�?设置"按钮左对齐展开时顶出右边缘被裁�?
|
||||
仅桌�?平板生效;≤640px 由移动端断点的全宽规则接管�?*/
|
||||
@media (min-width: 641px) {
|
||||
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .clone-ai-input-wrapper.ecom-command-composer > .ecom-command-popover.ecom-command-popover--settings {
|
||||
width: min(520px, calc(100% - 24px)) !important;
|
||||
@@ -12209,3 +12210,5 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
|
||||
margin: 0 !important;
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
}
|
||||
+5231
-6548
File diff suppressed because it is too large
Load Diff
+2450
-2450
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Ecommerce standalone color tokens — 蓝色/青色主题色板
|
||||
*
|
||||
* 这些变量替换了 base.css 和 overrides.css 中原先硬编码的 hex 颜色值。
|
||||
* 需要更换主题时只需修改此处即可。
|
||||
*/
|
||||
:root {
|
||||
/* Primary accent — buttons, links, active states */
|
||||
--ecom-primary: #1073cc;
|
||||
--ecom-primary-rgb: 16, 115, 204;
|
||||
--ecom-primary-hover: #0f66b3;
|
||||
--ecom-primary-light: rgba(16, 115, 204, 0.12);
|
||||
|
||||
/* Cyan / teal accent — gradients, focus rings, highlights */
|
||||
--ecom-accent-cyan: #1ebddb;
|
||||
--ecom-accent-cyan-rgb: 30, 189, 219;
|
||||
--ecom-accent-cyan-light: #16c8df;
|
||||
--ecom-accent-cyan-bright: #18bfd2;
|
||||
--ecom-accent-cyan-deep: #0f829b;
|
||||
|
||||
/* Background — white / near-white variants */
|
||||
--ecom-bg-white: #ffffff;
|
||||
--ecom-bg-near-white: #feffff;
|
||||
--ecom-bg-page: #f8f9fa;
|
||||
--ecom-bg-tinted: #edf8fb;
|
||||
--ecom-bg-cool: #f3f8fa;
|
||||
|
||||
/* Background — dark blue variants */
|
||||
--ecom-bg-dark: #10202c;
|
||||
--ecom-bg-dark-blue: #172636;
|
||||
--ecom-bg-deep-blue: #163447;
|
||||
--ecom-bg-navy: #083c67;
|
||||
--ecom-bg-navy-deep: #05233f;
|
||||
|
||||
/* Text */
|
||||
--ecom-text-primary: #10202c;
|
||||
--ecom-text-on-dark: #f5fbff;
|
||||
--ecom-text-muted: #738392;
|
||||
--ecom-text-placeholder: #8da0ab;
|
||||
|
||||
/* Border */
|
||||
--ecom-border-light: rgba(16, 115, 204, 0.14);
|
||||
--ecom-border-accent: rgba(30, 189, 219, 0.28);
|
||||
--ecom-border-cyan: rgba(29, 190, 219, 0.66);
|
||||
|
||||
/* Shadow */
|
||||
--ecom-shadow-primary: 0 10px 28px rgba(16, 115, 204, 0.08);
|
||||
--ecom-shadow-accent: 0 16px 34px rgba(29, 190, 219, 0.24);
|
||||
--ecom-shadow-cyan: 0 18px 46px rgba(15, 130, 155, 0.13);
|
||||
|
||||
/* Spacing */
|
||||
--ecom-space-xs: 6px;
|
||||
--ecom-space-sm: 8px;
|
||||
--ecom-space-md: 12px;
|
||||
--ecom-space-lg: 16px;
|
||||
--ecom-space-xl: 20px;
|
||||
--ecom-space-2xl: 24px;
|
||||
|
||||
/* Radius */
|
||||
--ecom-radius-sm: 8px;
|
||||
--ecom-radius-md: 10px;
|
||||
--ecom-radius-lg: 14px;
|
||||
--ecom-radius-xl: 16px;
|
||||
--ecom-radius-2xl: 18px;
|
||||
}
|
||||
Reference in New Issue
Block a user