@epxing/theme 0.0.0-stage → 5.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ // 图标底座:一个 class 字符串 → 一个 SVG mask。
2
+ //
3
+ // 兼容契约:`<i class="el-icon-search">` 的写法一个字不改,DB 里存的图标 class 照常工作。
4
+ // font-size 控大小(1em 跟随)、color 控颜色(currentColor),与字体图标完全等价,
5
+ // 于是暗黑模式与换肤令牌天然继续生效。
6
+ //
7
+ // ⚠️ 元素级样式是从原字体 CSS 里**照搬**过来的,不是新加的样式——但 el 族与 epx 族
8
+ // 今天的现状不一样,底座必须分两组精确复刻,不能合并成一份:
9
+ // · el 族:packages/theme/src/theme/element-icons/icon.css 头部规则给 <i> 设了
10
+ // `display:inline-block; line-height:1; vertical-align:baseline` + 两条 font-smoothing。
11
+ // · epx 族(非彩色):packages/theme/src/index.scss:75-83 只给了
12
+ // `display:inline-block` + 两条 font-smoothing——**没有 line-height,没有
13
+ // vertical-align**。epx 图标今天实际继承的是宿主上下文的 line-height(如 body 的
14
+ // 1.5),不是 1。
15
+ // 若「顺手」把 el 那份 line-height:1 也套到 epx 头上,是给 181 个 epx 图标净新增一次
16
+ // 行为改变:line-height 是继承属性,参与 inline-block 的行盒高度计算,1 与 1.5 算出的
17
+ // 元素高度不同——Task 1 的几何基线已经量到 epx 族 h:32、el 族 h:14,正是这条差异的实证。
18
+ // 这违反「长相零变化」这条唯一验收基准。撤掉字体 CSS 会把两族原有的声明一起带走,
19
+ // 那正是基线对齐崩掉的来源。改这段之前先看 e2e/tests/icon-geometry.spec.ts;也别把
20
+ // 下面两个 host 占位符合并成一份——这个不对称是现状的真实写照,不是疏漏。
21
+ //
22
+ // ⚠️ `:not([class*="epx-icon-color-"])`:P1 阶段彩色库仍走字体,而彩色类形如
23
+ // epx-icon-color-*,会被 [class^="epx-icon-"] 命中,需要排除掉、不让 mask 规则碰它。
24
+ //
25
+ // 下面第 123-126 行(::before 分支)的排除包了一层 `:where()`——写成 `:not(:where(...))` 而不是裸
26
+ // `:not(...)`:CSS 规范里 `:not()` 的特异度等于其参数本身的特异度,裸
27
+ // `:not([class*="epx-icon-color-"])` 会把这条 ::before 规则的特异度从 (0,1,1) 抬到
28
+ // (0,2,1),恒定盖过 `.generated/icons.css` 里逐图标规则的 (0,1,1)——这不是「稳赢字体
29
+ // 规则」的加分项,是误伤:连本该赢的逐图标规则也会被这条兜底渐变盖掉,与顺序无关。
30
+ // 本文件最早的写法就是这样(裸 `:not()`),后果是全部 181 个非彩色 epx-icon-* 图标
31
+ // 恒定渲染成透明——Task 6 修复轮 1 用 461 图标全量 `getComputedStyle` 断言 + 截图证实,
32
+ // 是设计时没预料到的连带影响,Task 3 两轮评审也没抓到,几何 fixture 更是天生测不出来
33
+ // (盒子尺寸/位置只由本规则的 content/display/width/height/vertical-align 决定,不受
34
+ // --epx-i 取哪个值影响,透明兜底照样量出一个「正确」的盒子)。
35
+ // `:where()` 的特异度贡献恒为 0(无论参数多复杂),排除的匹配效果与裸 `:not()`完全
36
+ // 一样(哪些元素被排除不变),但整条规则的特异度回到 (0,1,1),与逐图标规则重新打平,
37
+ // 由源码顺序决胜负——下面「本文件必须在 .generated/icons.css 之前 @use」那条顺序说明
38
+ // 因此仍然成立。`:where()` 需要 Chrome 88 / Safari 14+,本仓 mask 兜底路径的下限是
39
+ // iOS 15+,足够安全。
40
+ //
41
+ // 下面第 95-96 行那条应用 `%epx-icon-host-epx` 占位符的选择器(host 规则)**也**包了
42
+ // `:where()`,特异度回到 (0,1,0)——与改造前 packages/theme/src/index.scss:75-83 那条
43
+ // `[class*="epx-icon-"]` 逐字等价(普通属性子串选择器就是 (0,1,0)),排除彩色类的效果
44
+ // 完全不变。
45
+ //
46
+ // I3(终审 Important,收尾修复波):这里曾经的写法是裸 `:not()`,把特异度抬到 (0,2,0),
47
+ // 附带理由是「改低反而可能让别的 (0,1,0) 规则意外打平并靠源码顺序赢过它,凭空引入回归」——
48
+ // **这个论证把因果颠倒了**。(0,1,0) + 源码顺序决胜,正是改造前所有组件样式被写出来时
49
+ // 所处的世界;抬到 (0,2,0) 才是这次改造凭空引入的偏离,不是「维持现状」。真正的风险面也
50
+ // 不在仓库之内——产品项目普遍用单类选择器(如 `.my-btn .epx-icon-xxx{...}`,特异度
51
+ // (0,2,0))覆盖平台样式,它们看不见这次特异度变更,一旦平台侧的 host 规则是 (0,2,0),
52
+ // 产品项目那条本该赢的覆盖就会莫名其妙不生效——「我明明写了 display 却不生效」,且这类
53
+ // 项目在仓库外,本仓的测试与人眼复核都覆盖不到。`:where()` 消掉这个风险面,与 ::before
54
+ // 分支同一个修法、同一个理由(Ruling 20)。
55
+ //
56
+ // P3 彩色库转 SVG 后,这整层 `:not(:where(...))` 排除可以一起摘掉。
57
+ //
58
+ // ⚠️ 本文件在加载顺序上必须排在 .generated/icons.css **之前**——结论没变,但机制已经变了
59
+ // (M2/M14,终审 Minor,收尾修复波):icons.css 现在不走 @use,是
60
+ // packages/core/src/plugins/element-plus.js 动态 `import()` 进来的一份独立异步 chunk
61
+ // (见该文件、及本文件头部「fix round 1」注释)。本文件(随 element-plus.js 的静态 import
62
+ // eager 打进首屏 CSS)与 icons.css 的 ::before 兜底规则特异度打平(都是 0,1,1)——顺序
63
+ // 反了(icons.css 先于本文件生效)每个图标会静默变空,原因不变,只是「顺序」现在由
64
+ // 「谁先执行到那行 import()」决定,不再是 Sass @use 的静态书写顺序。
65
+ //
66
+ // ⚠️ dev 期间的假故障:改本文件会触发 Vite HMR,把它重新追加到当前样式流的**末尾**——
67
+ // 排到早已异步加载完的 icons.css 之后,于是全部图标当场变透明。这和本期修的 Ruling 20
68
+ // 那个 Critical(:not() 特异度打平后靠顺序决胜负)长得一模一样,容易误诊成代码回归。
69
+ // **硬刷新即可**,不用回滚代码。
70
+
71
+ // el 族:与 packages/theme/src/theme/element-icons/icon.css 头部规则等价
72
+ %epx-icon-host-el {
73
+ display: inline-block;
74
+ line-height: 1;
75
+ vertical-align: baseline;
76
+ font-style: normal;
77
+ -webkit-font-smoothing: antialiased;
78
+ -moz-osx-font-smoothing: grayscale;
79
+ }
80
+
81
+ // epx 族(非彩色):与 packages/theme/src/index.scss:75-83 等价——不含
82
+ // line-height / vertical-align,跟上面 el 族的差异是现状本来如此,不是漏写
83
+ %epx-icon-host-epx {
84
+ display: inline-block;
85
+ font-style: normal;
86
+ -webkit-font-smoothing: antialiased;
87
+ -moz-osx-font-smoothing: grayscale;
88
+ }
89
+
90
+ [class^="el-icon-"],
91
+ [class*=" el-icon-"] {
92
+ @extend %epx-icon-host-el;
93
+ }
94
+
95
+ [class^="epx-icon-"]:where(:not([class*="epx-icon-color-"])),
96
+ [class*=" epx-icon-"]:where(:not([class*="epx-icon-color-"])) {
97
+ @extend %epx-icon-host-epx;
98
+ }
99
+
100
+ // `ri-*`:直接写 Remix 图标名,不必先在映射表里起个平台名。
101
+ //
102
+ // 461 个 el-icon-*/epx-icon-* 是**为兼容数据库里的存量值**而存在的,新写的代码没道理
103
+ // 还要先起名再映射。构建期扫源码里出现的 ri-xxx,只为**用到的那些**生成规则(按需,
104
+ // 不是把 Remix 3229 个全打进去),见 build/icons/scan-direct-icons.mjs。
105
+ //
106
+ // ⚠️ 这条路只覆盖**源码里写死的**。数据库里配的图标名扫描看不见——库里填 ri-xxx 会
107
+ // 静默不显示(没有对应规则 ⇒ --epx-i 停在下面的透明兜底)。DB 场景仍走平台名,
108
+ // 见 docs/guide/tools/icon.md。
109
+ //
110
+ // 前缀安全性:实测全仓 997 个源码文件里以 ri- 开头的 class **零命中**,不与任何既有
111
+ // 类名冲突(本仓从未用过 Remix 官方 CSS,也没引过它的字体)。
112
+ [class^="ri-"],
113
+ [class*=" ri-"] {
114
+ @extend %epx-icon-host-epx;
115
+ }
116
+
117
+ // I1(终审 Important,收尾修复波):`.epx-icon` 是**精确类**(不带尾横线),原规则在
118
+ // public/font/font_2530548/iconfont.css:8-14,随该文件的 <link> 一起被 index.html 撤走。
119
+ // 复刻它只需要 font-size 这一条——display/font-style/两条 font-smoothing 已经由上面
120
+ // %epx-icon-host-epx 给 epx 族全体复刻过了(凡是命中 `.epx-icon` 的元素,如
121
+ // icon/Icon.vue:2 的 `epx-icon ${name}`,class 属性里同时带着 epx-icon-xxx 前缀类,
122
+ // 一样会命中上面的 host 选择器),不用重复。
123
+ //
124
+ // ⚠️ 曾经的误判(执行报告 §5 里那条已删的债):以为 `.epx-icon` 是前缀类,补它会连带给
125
+ // 「裸写 epx-icon-xxx(class 属性里根本没有独立的 epx-icon 这个词)」的 192 处也套上
126
+ // 16px,构成新的长相改变,因此当时决定不补。但这里没有 `^=`/`*=` 通配,是精确类选择器:
127
+ // 只命中 class 属性里真的有一个独立 `epx-icon` 词的元素,碰不到裸写的 192 处。补它是纯
128
+ // 缺陷修复,不是新增行为。
129
+ //
130
+ // 缺了它的后果:`<i>` 元素自身的 font-size 不再显式设成 16px,改吃继承值(多数上下文是
131
+ // body 的 14px)——::before 规则的 width/height 是 1em,随宿主元素的 font-size 走,图标
132
+ // 因此从 16px 缩到 14px(−12.5%,肉眼可见)。受影响的至少有:icon/Icon.vue(页面配置里
133
+ // `component: 'icon'` 的渲染点,用量存在数据库里,守卫扫不到)、
134
+ // designer/page-designer/previews/icon/Preview.vue、app-nav/AppNav.vue 的全屏按钮、以及
135
+ // 走 <epx-icon> 的 ManageMenu.vue / ManageMenuItem.vue。
136
+ .epx-icon {
137
+ // 令牌化(P2b-1 Task1):兜底仍是 16px ⇒ PC 侧与图标 e2e fixture 的计算值逐字节不变。
138
+ // ⚠️ 真实机制(终审核实,已读 build/icons/make-fixture.mjs 源码确认):图标几何/光学基线
139
+ // 不受影响,**不是**因为「两个 spec 都用自包含 setContent、根元素无 data-viewport」——那只是
140
+ // 第二道保险,见下。真正的原因是 make-fixture.mjs 的 rows() 给每个 <i> 只套**单一**字形类
141
+ // (如 `epx-icon-search`),从不额外挂裸 `epx-icon` token;而这条规则的选择器 `.epx-icon`
142
+ // 是**精确类**,只命中 class 属性里真的带独立 `epx-icon` 词的元素 ⇒ 这条规则从未命中过
143
+ // fixture 里的任何 <i>,--epx-icon-size 根本没被这些元素消费,与 data-viewport 存不存在、
144
+ // 兜底值是 16px 还是别的都无关——规则压根没打在元素上。
145
+ // 「根元素无 data-viewport」只是第二道保险,目前是备胎不是主防线:若将来有人把 fixture
146
+ // 改得更贴近真实用法(给 <i> 补上裸 epx-icon token,使其命中上面这条规则),届时这条规则
147
+ // 才会真正吃到 --epx-icon-size,「无 data-viewport ⇒ 兜底 16px」才会从备胎变成唯一防线。
148
+ // 移动端由 :root[data-viewport="mobile"] 的 --epx-icon-size 抬到 20px。
149
+ // ⚠️ 改这条兜底值 = 改 PC 图标长相 = 碰红图标基线,动之前先知会图标那条线。
150
+ font-size: var(--epx-icon-size, 16px);
151
+ }
152
+
153
+ [class^="el-icon-"]::before,
154
+ [class*=" el-icon-"]::before,
155
+ [class^="epx-icon-"]:where(:not([class*="epx-icon-color-"]))::before,
156
+ [class*=" epx-icon-"]:where(:not([class*="epx-icon-color-"]))::before,
157
+ [class^="ri-"]::before,
158
+ [class*=" ri-"]::before {
159
+ // 缺省是全透明渐变:万一某个 class 误命中(如 BEM 修饰符 el-icon--right),
160
+ // 画出来是透明的,而不是一个 1em 见方的 currentColor 实心块。
161
+ // 实测这个兜底不增加 gzip(重复串压缩掉了),等于白拿一份保险。
162
+ --epx-i: linear-gradient(#0000, #0000);
163
+
164
+ content: '';
165
+ display: inline-block;
166
+ width: 1em;
167
+ height: 1em;
168
+ background-color: currentColor;
169
+ -webkit-mask: var(--epx-i) no-repeat center / 100%;
170
+ mask: var(--epx-i) no-repeat center / 100%;
171
+ vertical-align: -0.125em;
172
+ }
173
+
174
+ // 旋转动画:原来在 element 的 icon.css 里,随字体一起撤走,这里补回
175
+ .el-icon-loading {
176
+ animation: epx-icon-rotating 2s linear infinite;
177
+ }
178
+
179
+ @keyframes epx-icon-rotating {
180
+ from { transform: rotate(0); }
181
+ to { transform: rotate(360deg); }
182
+ }
183
+
184
+ // 间距工具类:BEM 修饰符,不是图标名。原在 icon.css 里,一并搬过来
185
+ .el-icon--right { margin-left: 5px; }
186
+ .el-icon--left { margin-right: 5px; }
@@ -0,0 +1,30 @@
1
+ // 图标结构入口的【库模式变体】——与 index.scss 的唯一差异:把 461 条逐图标规则也静态
2
+ // @use 进来,不依赖 packages/core/src/plugins/element-plus.js 里那条给平台应用首屏用的
3
+ // 动态 import()。
4
+ //
5
+ // 为什么需要单独一份(收尾修复波裁决:库走静态,应用走异步):JS 动态 import() 拉 CSS
6
+ // 这件事,在 Vite library 模式下语义不成立——实测(报告 §0c):CJS 多入口输出把它编译成
7
+ // require() 一个从未生成的 chunk(package.json 的 main/require 消费方触发时直接抛
8
+ // `Cannot find module`);ESM 分模块输出编译成 Promise.resolve({}) 空操作(静默丢图标,
9
+ // 不报错,比前者更隐蔽)。根因是 Ruling 18 的代价:那条 import() 是专门为平台应用的首屏
10
+ // 预算设计的(不阻塞首屏),库本来就不需要这个语义——库的消费方拿到的是产物,自己决定
11
+ // 怎么分包。正解不是想办法让动态 import 在库模式下也 work,而是两条路径各走各的:
12
+ // 平台应用(vite.config.mjs)继续走 index.scss + 动态 import;五个库/包/远程构建
13
+ // (vite.lib*.config.mjs / build/vite.pkg.config.mjs / build/vite.remote.config.mjs)
14
+ // 把 @epxing/theme/src/theme/icons/index.scss 的别名指到本文件(见各配置 resolve.alias,
15
+ // 必须排在 epxAliases()/@epxing/theme 之前——前缀匹配、先命中者赢),同时把
16
+ // __EPX_ICON_ASYNC__ 这个构建期常量 define 成 false,让 element-plus.js 里那条 import()
17
+ // 连同所在的 if 块一起被 tree-shake——避免 461 条规则在库产物里重复出现。
18
+ //
19
+ // ⚠️ 顺序不能反:@use './base.scss' 必须在 @use './.generated/icons.css' 之前。两者的
20
+ // ::before 规则特异度打平(都是 0,1,1),靠源码顺序决胜负——base.scss 的透明兜底规则若
21
+ // 排在逐图标规则之后生效,会覆盖掉每一条 --epx-i 的具体取值,全部图标变透明(Ruling 20
22
+ // 踩过的同一个坑,机制一致)。
23
+ //
24
+ // ⚠️ @use 一个 .css 文件是 Dart Sass 支持的既有能力(当纯 CSS 内联处理,不解析
25
+ // 变量/mixin)——本仓在 fix round 1 拆分动态/静态之前,index.scss 本身就是这么用的
26
+ // (@use './.generated/icons.css'),不是本文件引入的新用法。也不会触发
27
+ // vite.config.mjs 里 EPX_ICON_LEGACY 别名旁警告过的 url() 误 rebase 问题:.generated/
28
+ // icons.css 里全是 data:image/svg+xml 内联,没有任何相对路径的 url() 引用。
29
+ @use './base.scss';
30
+ @use './.generated/icons.css';
@@ -0,0 +1,21 @@
1
+ // 图标结构底座入口。EPX_ICON_LEGACY=1 时 vite.config.mjs 把本文件的别名直接指到
2
+ // packages/theme/src/theme/element-icons/icon.css(决策 A 的原字体 CSS),于是整套退回
3
+ // 字体路径(字体文件仍在仓库里,零成本);本文件届时完全不参与编译。开关不指到某个
4
+ // @use 本文件的 .scss 转接层——那样会让 Vite 把 icon.css 里的相对 url() 按错误的目录
5
+ // rebase,字体文件在产物里彻底丢失(原理见 vite.config.mjs 该别名旁的注释)。
6
+ //
7
+ // fix round 1:本文件只剩 base.scss(结构 —— display/line-height/透明兜底)。
8
+ // 逐图标规则(.generated/icons.css,461 条,体积大头)与 :root 变量(.generated/icon-vars.css,
9
+ // 3 处历史遗留调用点专用)都不在这里 @use 了,改由 packages/core/src/plugins/element-plus.js
10
+ // 直接引入:前者异步(不阻塞首屏),后者无条件 eager(两条路径都要、体积可忽略)。
11
+ // 三者互不冲突——base.scss 的兜底规则与 icons.css 的具体规则之间的加载顺序讲究
12
+ // (见 base.scss 文件头注释)在「谁先出现在同一个 CSS 输出流里」这件事上依然成立:
13
+ // icons.css 是异步追加的独立 chunk,进 DOM 时永远晚于本文件跟随主包一起到达的内容。
14
+ //
15
+ // ⚠️ 这套"异步"只对平台应用(vite.config.mjs)成立。五个库/包/远程构建
16
+ // (vite.lib*.config.mjs / build/vite.pkg.config.mjs / build/vite.remote.config.mjs)
17
+ // 把本文件的别名指到同目录的 index.lib.scss——那份变体把 461 条规则也静态 @use 进来,
18
+ // 理由见该文件头注释(收尾修复波实测:动态 import 拉 CSS 在 Vite library 模式下语义
19
+ // 不成立)。改本文件时留意:这里的结构改动(base.scss 的内容)两条路径都会吃到,但
20
+ // 加载时机的注释只对平台应用成立。
21
+ @use './base.scss';
@@ -0,0 +1,132 @@
1
+ /* 彩色图标库(iconfont.cn 项目 2612462,COLR/CPAL,24 个字形)。2026-09-02 从
2
+ * public/font/font_2612462/ + index.html 的 <link> 挪到这里,由
3
+ * packages/core/src/plugins/element-plus.js 静态 import。只留 woff2(woff/ttf 是 IE 回退,
4
+ * 本平台进不了 IE,永远选不中,已删)。
5
+ *
6
+ * @font-face 的字体文件是浏览器**用到才下**的:页面上没有命中宿主规则
7
+ * (packages/theme/src/index.scss 的 [class*="epx-icon-color-"])的元素时,这 28.6K 一个
8
+ * 字节都不下。所以这份 CSS 走 eager 只多约 2K,异步化换不来字节、只会换来图标闪一下。
9
+ *
10
+ * 其余缘由记在两处:为什么 eager、为什么是 .css 不是 .scss(sass 会按外层目录 rebase
11
+ * url(),字体静默丢失)→ core/plugins/element-plus.js 那条 import 上方;
12
+ * 为什么这 24 个不抠成内联 SVG(实测比字体大 35%)→ build/icons/color-icons.mjs 头注释。
13
+ *
14
+ * ⚠️ `?no-inline` 千万别删。Vite 在 **library 模式下无条件把资源 base64 内联**,
15
+ * `build.assetsInlineLimit` 在那条路径上根本走不到(vite 6.4 源码 shouldInline():
16
+ * `if (environment.config.build.lib) return true` 排在读 limit 之前,只有第一行的
17
+ * `noInlineRE = /[?&]no-inline\b/` 能否决它)。删掉的后果实测过:`pnpm build:component`
18
+ * 把这 28.6K 字体编码成 38K base64 塞进 lib/assets/element-plus-*.css,该文件
19
+ * 189K → 229K —— 于是消费方的**主样式表**里多背约 29K gzip,而且从「用到才下的独立文件」
20
+ * 变成「render-blocking CSS 里永远随行」,正好把本次改造想达成的效果反转掉。
21
+ * 平台构建不走 lib 模式、28.6K 也远超 assetsInlineLimit(2000),本来就出独立文件,
22
+ * 加这个 query 对平台是零影响的空操作。
23
+ */
24
+ @font-face {
25
+ font-family: "epx-icon-color"; /* Project id 2612462 */
26
+ /* Color fonts */
27
+ src: url('./fonts/iconfont.woff2?no-inline') format('woff2');
28
+ }
29
+
30
+ .epx-icon-color {
31
+ font-family: "epx-icon-color" !important;
32
+ font-size: 16px;
33
+ font-style: normal;
34
+ -webkit-font-smoothing: antialiased;
35
+ -moz-osx-font-smoothing: grayscale;
36
+ }
37
+
38
+ .epx-icon-color-kong1:before {
39
+ content: "\e706";
40
+ }
41
+
42
+ .epx-icon-color-a-404:before {
43
+ content: "\e6fe";
44
+ }
45
+
46
+ .epx-icon-color-yinyong:before {
47
+ content: "\e6f9";
48
+ }
49
+
50
+ .epx-icon-color-tab:before {
51
+ content: "\e6fa";
52
+ }
53
+
54
+ .epx-icon-color-tingyongtishi:before {
55
+ content: "\e6f4";
56
+ }
57
+
58
+ .epx-icon-color-feishulogo:before {
59
+ content: "\e6f0";
60
+ }
61
+
62
+ .epx-icon-color-yejishenbao:before {
63
+ content: "\e6ea";
64
+ }
65
+
66
+ .epx-icon-color-nianduhuibao:before {
67
+ content: "\e6eb";
68
+ }
69
+
70
+ .epx-icon-color-kaohejieguo:before {
71
+ content: "\e6ec";
72
+ }
73
+
74
+ .epx-icon-color-tianjiabaobiao:before {
75
+ content: "\e6e6";
76
+ }
77
+
78
+ .epx-icon-color-tianjiazujian:before {
79
+ content: "\e6e7";
80
+ }
81
+
82
+ .epx-icon-color-yichuli:before {
83
+ content: "\e6e0";
84
+ }
85
+
86
+ .epx-icon-color-caozuochenggong:before {
87
+ content: "\e6df";
88
+ }
89
+
90
+ .epx-icon-color-liulanqibuzhichi:before {
91
+ content: "\e6d9";
92
+ }
93
+
94
+ .epx-icon-color-kaohetongji:before {
95
+ content: "\e6d1";
96
+ }
97
+
98
+ .epx-icon-color-mubiaozhiding:before {
99
+ content: "\e6d2";
100
+ }
101
+
102
+ .epx-icon-color-kaohedengji:before {
103
+ content: "\e6d3";
104
+ }
105
+
106
+ .epx-icon-color-kaohefangan:before {
107
+ content: "\e6d4";
108
+ }
109
+
110
+ .epx-icon-color-kaohezhuti:before {
111
+ content: "\e6d5";
112
+ }
113
+
114
+ .epx-icon-color-yidongduankaohe:before {
115
+ content: "\e6d6";
116
+ }
117
+
118
+ .epx-icon-color-daorukaohejieguo:before {
119
+ content: "\e6d7";
120
+ }
121
+
122
+ .epx-icon-color-faqiniandukaohe:before {
123
+ content: "\e6d8";
124
+ }
125
+
126
+ .epx-icon-color-a-500:before {
127
+ content: "\e6cc";
128
+ }
129
+
130
+ .epx-icon-color-kong:before {
131
+ content: "\e6b7";
132
+ }
@@ -0,0 +1,275 @@
1
+ @use "sass:color";
2
+ // epx 设计令牌层(P1)——暗黑模式/字号/圆角/间距/语义色的**唯一维护点**。
3
+ //
4
+ // 结构:
5
+ // :root 基础令牌(亮色缺省值)
6
+ // [data-theme="dark"] 同名变量重定义 = 暗黑模式(与现有 data-theme 换肤同轨,零新机制)
7
+ // EP 结构映射 --el-border-radius/--el-font-size 引用 epx 令牌 → EP 全组件自动跟随
8
+ // 少量语义 utility .epx-p-*/.epx-gap-*/.epx-radius(间距/圆角类,页面配置 css 也可用)
9
+ //
10
+ // 使用(组件 <style> 内,mixins.scss 已全局注入无需 import):
11
+ // @include epx-card; @include epx-text(s); gap: var(--epx-space-2);
12
+ // 主题色仍走既有 --color-primary 体系(element-plus-tokens.scss 12 套主题 + custom)。
13
+ // EP 暗黑色板:core/install.js 引 element-plus/theme-chalk/dark/css-vars.css(挂 html.dark,
14
+ // 主题应用处同步 class)。看板/echarts/设计器不吃 CSS 变量,暗黑适配是 P3。
15
+
16
+ :root {
17
+ // 主题色(RGB 三元组,rgba(var(--color-primary), a) 用;custom 主题运行时覆写)
18
+ --color-primary: 8,116,231;
19
+ // 字号(PC 基准 14)
20
+ --epx-font-size-s: 12px;
21
+ --epx-font-size-base: 14px;
22
+ --epx-font-size-l: 16px;
23
+ --epx-font-size-xl: 20px;
24
+
25
+ // 圆角
26
+ --epx-radius-s: 2px;
27
+ --epx-radius-base: 4px;
28
+ --epx-radius-l: 8px;
29
+
30
+ // 间距(4 的倍数)
31
+ --epx-space-1: 4px;
32
+ --epx-space-2: 8px;
33
+ --epx-space-3: 12px;
34
+ --epx-space-4: 16px;
35
+ --epx-space-5: 24px;
36
+ --epx-space-6: 32px;
37
+
38
+ // 语义色(亮色缺省;暗黑在下方整组重定义)
39
+ --epx-bg: #f5f7fa;
40
+ --epx-bg-card: #ffffff;
41
+ --epx-text: #303133;
42
+ --epx-text-secondary: #909399;
43
+ --epx-border: #dcdfe6;
44
+
45
+ // ── 文字色梯度(2026-08-20 从 index.scss 的 :root 迁来)─────────────────
46
+ // 原先这五个定义在 index.scss:531 附近,而 index.scss 第 4 行才 @import 本文件
47
+ // ⇒ **暗色重定义写在本文件里会被后面的 :root 覆盖掉**(`:root` 与 `[data-theme=dark]`
48
+ // 特异性相同,同分靠源码顺序决胜,谁在后谁赢)。结果就是:它们看着像令牌、
49
+ // /ui-standard 规范里还推荐优先使用,实际**和硬编码一样不跟随暗色**。
50
+ // 迁到这里之后明暗两块同文件、暗色块在后,重定义才真正生效。
51
+ // ⚠️ 亮色取值逐字节保持原样,亮色观感零变化。
52
+ --color-text-title: #222222;
53
+ --color-text-primary: #444444;
54
+ --color-text-secondary: #666666;
55
+ --color-text-assistant: #999999;
56
+ --color-text-disabled: #aaaaaa;
57
+
58
+ // 密度轴驱动的外边距(默认 10px == 改造前 index.scss 的 --margin-base)
59
+ --epx-margin: 10px;
60
+
61
+ // 阴影(默认值逐字节抄自 Element Plus theme-chalk 编译产物,保证 EP 组件观感不变)
62
+ --epx-shadow-s: 0px 0px 6px #0000001f;
63
+ --epx-shadow-base: 0px 0px 12px #0000001f;
64
+ --epx-shadow-l: 0px 12px 32px 4px #0000000a, 0px 8px 20px #00000014;
65
+ --epx-shadow-xl: 0px 16px 48px 16px #00000014, 0px 12px 32px #0000001f, 0px 8px 16px -8px #00000029;
66
+ // 焦点环:跟随主题色(修掉散落的硬编码蓝 rgba(22,119,255,.1) / rgba(64,158,255,.3))
67
+ --epx-shadow-focus: 0 0 0 2px rgba(var(--color-primary), 0.1);
68
+ // TODO(P1 阴影收敛):--epx-shadow-s/base/l/xl 在下面的 [data-theme=dark], .dark 块里**没有暗色重定义**,
69
+ // 亮色值会原样继承到暗色。今天零影响——它们唯一的消费者是上面的 --epx-shadow-focus(跟随主题色,
70
+ // 与明暗无关),而 EP 组件的暗色投影由 element-plus/theme-chalk/dark/css-vars.css 的 html.dark 自己给。
71
+ // 但 P1 的既定任务是把全站 189 处硬编码 box-shadow 收敛到这四个令牌上——那一刻起,每一处收敛过来的
72
+ // 阴影都会在暗色下用亮色投影(#0000001f 这种浅黑在深底上几乎看不见,表现为「暗色下卡片没有层次」)。
73
+ // 所以 P1 动手前必须先补一组暗色阴影值(EP 暗色用的是 #000000b8 量级的更重投影,可作参照)。
74
+ // 这里刻意不自行发明取值:那是设计决策,不该在修复轮里定。
75
+
76
+ // 遗留变量桥接:--border-radius(37 处消费) / --margin(30 处消费) 改吃令牌,
77
+ // 于是 AppIndex.setComponentStyle() 的 JS 拼 CSS 注入可以整体退役(见 Task 4)。
78
+ // 注意 index.scss:522-523 也定义了这两个变量,本文件在 index.scss 里先于它被 @import,
79
+ // 故 Task 4 会把 index.scss 里那两行删掉,避免后写覆盖本处。
80
+ --border-radius: var(--epx-radius-base);
81
+ --margin: var(--epx-margin);
82
+ }
83
+
84
+ // ── EP 结构性变量映射:改 epx 令牌 = EP 全组件跟随(颜色映射在 element-plus-tokens.scss)──
85
+ //
86
+ // ⚠️ 选择器必须是 `html:root` (0,1,1) 而不是裸 `:root` (0,1,0),与 vant-tokens.scss 同理:
87
+ // Element Plus 自带的 theme-chalk/base.css 也在裸 `:root` (0,1,0) 定义这些同名变量,同特指度只能
88
+ // 靠文档顺序裁决、而顺序不由本文件控制(打包分片/按需引入都可能改变它)。一旦 EP 后插入,圆角轴与
89
+ // 字号轴就对全平台 EP 组件静默失效——校验脚本照样绿,肉眼也只在切轴时才发现。抬到 (0,1,1) 后与
90
+ // 加载顺序无关。build/__check-style-axes.mjs 有守卫钉住这一点。
91
+ //
92
+ // ⚠️⚠️ 只有 `--el-*` 映射能抬。`--color-primary` 和所有 `--epx-*` 必须留在上面的裸 `:root`:
93
+ // 11 套预设主题的 `[data-theme=<name>]` 块是 (0,1,0),靠源序压过 `:root` 来覆写 `--color-primary`;
94
+ // 把 `:root` 抬成 `html:root` (0,1,1) 会让那 11 个块再也压不过它 → 全部主题色集体失效。
95
+ // 同理 `:root[data-radius=…]` 等四轴块是 (0,2,0),靠覆写 `--epx-*` 生效,也要求宿主留在裸 `:root`。
96
+ //
97
+ // 注:`--el-box-shadow-*` 在暗色下另有 EP 自己的 `html.dark` (0,1,1) 重定义(深底要更重的投影)。
98
+ // 抬到 `html:root` 后两者特指度相同、由文档顺序裁决,而 core/install.js 是先 `@epxing/theme/src/index.scss`
99
+ // 后 `element-plus/theme-chalk/dark/css-vars.css` → EP 暗色仍然在后、仍然赢,暗色观感不变。
100
+ // 扁平档不受影响:`:root[data-elevation=flat]` 是 (0,2,0),压得过上述两者。
101
+ html:root {
102
+ --el-border-radius-base: var(--epx-radius-base);
103
+ --el-border-radius-small: var(--epx-radius-s);
104
+ --el-font-size-base: var(--epx-font-size-base);
105
+ --el-font-size-small: var(--epx-font-size-s);
106
+ --el-font-size-large: var(--epx-font-size-l);
107
+
108
+ // 默认档取值与 EP 原值逐字节相同(零变化),[data-elevation=flat] 时整体置 none
109
+ --el-box-shadow-lighter: var(--epx-shadow-s);
110
+ --el-box-shadow-light: var(--epx-shadow-base);
111
+ --el-box-shadow: var(--epx-shadow-l);
112
+ --el-box-shadow-dark: var(--epx-shadow-xl);
113
+ }
114
+
115
+ // 暗黑模式:既吃旧的 [data-theme=dark],也吃 html.dark(pure-admin 式主题模式与主题色解耦——
116
+ // html.dark 控暗黑、data-theme 控主题色,二者可叠加:data-theme=purple + html.dark = 紫底暗黑)
117
+ [data-theme="dark"],
118
+ .dark {
119
+ --epx-bg: #141414;
120
+ --epx-bg-card: #1d1e1f;
121
+ --epx-text: #e5eaf3;
122
+ --epx-text-secondary: #a3a6ad;
123
+ --epx-border: #4c4d4f;
124
+
125
+ // 文字色梯度的暗色档。取值对齐 --epx-text / --epx-text-secondary 的暗色基调,
126
+ // 保持「标题最亮 → 禁用最暗」的原有层次关系(亮色下是 #222→#aaa 递浅,
127
+ // 暗色下反过来 #e5eaf3→#6c6e72 递深,对比度方向随底色翻转)。
128
+ --color-text-title: #e5eaf3;
129
+ --color-text-primary: #cfd3dc;
130
+ --color-text-secondary: #a3a6ad;
131
+ --color-text-assistant: #8d9095;
132
+ --color-text-disabled: #6c6e72;
133
+
134
+ color-scheme: dark; // 原生滚动条/表单控件跟随
135
+ }
136
+
137
+ // ── 风格轴(UI P0)──────────────────────────────────────────────
138
+ // 四个轴各是 <html> 上的一个 data-* 属性,互相正交,也与 data-theme(主题色) / .dark(暗黑) 正交。
139
+ // **属性缺省 = 默认档**:默认值写在上面的 :root 里,这里只写非默认档,
140
+ // 于是「没设过风格」的存量用户拿到的计算值与改造前逐值相同。
141
+ //
142
+ // 圆角 4 档 / 密度 4 档而非 3 档,是为了逐值命中遗留的应用级配置
143
+ // (pageConfig.borderRadius medium=8px large=12px;componentMargin small=6 medium=8 large=12)。
144
+
145
+ :root[data-radius="sharp"] { --epx-radius-s: 0px; --epx-radius-base: 2px; --epx-radius-l: 4px; }
146
+ :root[data-radius="round"] { --epx-radius-s: 4px; --epx-radius-base: 8px; --epx-radius-l: 12px; }
147
+ :root[data-radius="extra"] { --epx-radius-s: 6px; --epx-radius-base: 12px; --epx-radius-l: 20px; }
148
+
149
+ :root[data-font="small"] {
150
+ --epx-font-size-s: 12px;
151
+ --epx-font-size-base: 13px;
152
+ --epx-font-size-l: 15px;
153
+ --epx-font-size-xl: 18px;
154
+ }
155
+ :root[data-font="large"] {
156
+ --epx-font-size-s: 13px;
157
+ --epx-font-size-base: 16px;
158
+ --epx-font-size-l: 18px;
159
+ --epx-font-size-xl: 22px;
160
+ }
161
+
162
+ // 密度轴 P0 只驱动 --epx-margin(等价替换 setComponentStyle 的 --margin 注入)。
163
+ // EP 控件高度与表格行高留到 P1 —— 那会改观感,本期不碰。
164
+ :root[data-density="compact"] { --epx-margin: 6px; }
165
+ :root[data-density="cozy"] { --epx-margin: 8px; }
166
+ :root[data-density="loose"] { --epx-margin: 12px; }
167
+
168
+ // 层次轴:扁平档去掉全部投影。选择器写成 :root[...] (0,2,0) 而非 [...] (0,1,0),
169
+ // 好压过 element-plus/theme-chalk/dark/css-vars.css 里 `html.dark {}` 对 --el-box-shadow-* 的重定义
170
+ // ——注意那个块的选择器是 `html.dark` (0,1,1) 而不是裸 `.dark` (0,1,0)(已核对 EP 2.14.3 产物),
171
+ // (0,2,0) 两者都压得过,所以结论不变:暗色下切扁平同样生效。
172
+ :root[data-elevation="flat"] {
173
+ --epx-shadow-s: none;
174
+ --epx-shadow-base: none;
175
+ --epx-shadow-l: none;
176
+ --epx-shadow-xl: none;
177
+ --el-box-shadow-lighter: none;
178
+ --el-box-shadow-light: none;
179
+ --el-box-shadow: none;
180
+ --el-box-shadow-dark: none;
181
+ }
182
+
183
+ // ── 移动轴(UI 移动端重构 P1)────────────────────────────────────────
184
+ // 触发源不是媒体查询,是 <html data-viewport="mobile">,由 packages/core/src/viewport.js
185
+ // 从 store 的 isMobile 同步写入。
186
+ //
187
+ // 为什么不用 @media (max-width: 992px):mobileWidth 是**可配的**(AppIndex.vue 读应用配置
188
+ // commit SET_MOBILE_WIDTH,PageDesigner 也会改)。媒体查询里写死 992 会与 $isMobile 的判定
189
+ // 漂移,出现「JS 认为是移动端、CSS 认为不是」的错位——而且只在改过 mobileWidth 的系统上复现。
190
+ // 走属性则与 $isMobile 同一个真相源,永不漂移。
191
+ //
192
+ // ⚠️⚠️ 本块特指度 (0,2,0),与上面四个风格轴**完全相同**,同分靠源码顺序决胜。
193
+ // 所以本块**绝对不能重定义四轴驱动的令牌**(--epx-font-size-* / --epx-radius-* /
194
+ // --epx-margin / --epx-shadow-*)——抢了就会让用户在移动端切字号/圆角/密度/层次静默失效。
195
+ // 本块只做两件事:① 新增移动专属令牌(PC 侧无定义)② 覆写 --el-* 控件尺寸。
196
+ // test/unit/mobile-token-axis.test.js 有门禁钉住这一点。
197
+ //
198
+ // 覆写 --el-component-size / --el-input-height 是安全的,但宿主**不是**上面的 html:root——
199
+ // html:root 从不映射这两个变量(它只映射 --el-border-radius-*/--el-font-size-*/
200
+ // --el-box-shadow-*,见上面那个块)。真正压过的对手是 Element Plus 自己的裸 :root (0,1,0)
201
+ // (theme-chalk/base.css 定义 --el-component-size:32px),(0,2,0) 稳赢。--el-input-height
202
+ // 在 EP 的 .el-input 组件里被局部重新声明为 var(--el-component-size)(theme-chalk/
203
+ // el-input.css)——声明在元素自己身上,任何祖先规则都盖不了它,只能靠 --el-component-size
204
+ // 的继承链间接改变它的值;这里直接覆写的 --el-input-height 则是留给**不经过 .el-input
205
+ // 自己重新声明**的消费方。完整链路 + 同源哨兵见 e2e/tests/mobile-atoms.spec.ts
206
+ // (2026-08-22 review:这条注释曾经写错——错把 html:root 当成过宿主,已订正)。
207
+ :root[data-viewport="mobile"] {
208
+ // ① 移动专属令牌
209
+ --epx-touch-min: 44px; // 最小触控目标(iOS HIG 44pt / Material 48dp 取下限)
210
+ --epx-m-gutter: 16px; // 页面左右安全边距
211
+ --epx-m-gap: 12px; // 卡片间距
212
+ --epx-m-radius-card: 12px; // 卡片圆角(移动端惯例比 PC 的 --epx-radius-l 8px 更大)
213
+ --epx-m-radius-sheet: 16px; // 底部弹层顶部圆角
214
+ --epx-m-nav-height: 44px; // 顶栏
215
+ --epx-m-tab-height: 50px; // 底导(不含安全区)
216
+ // index.html 已有 viewport-fit=cover,env() 才有非零值;旧 WebView 走 0px 兜底。
217
+ --epx-safe-top: env(safe-area-inset-top, 0px);
218
+ --epx-safe-bottom: env(safe-area-inset-bottom, 0px);
219
+ // 图标字号。base.scss 的 .epx-icon 原本硬编码 16px(逐字复刻被撤走的字体 CSS 那条规则,
220
+ // 为的是守住图标 P1「长相零变化」的验收基准)⇒ 不响应本轴,移动端 44px 触控尺度下偏小。
221
+ // 这里给移动端一个更大的值;PC 侧**不定义**,base.scss 用 var(..., 16px) 兜底
222
+ // ⇒ PC 与图标 e2e fixture 的计算值逐字节不变——真实原因是 fixture 从不给 <i> 挂裸 epx-icon
223
+ // token,.epx-icon 精确类规则从未命中它(与「根元素无 data-viewport」无关,那只是没被触发的
224
+ // 第二道保险);完整机制见 base.scss 的 .epx-icon 规则注释。
225
+ // 20/44 ≈ 45%,符合 iOS HIG 与 Material 的图标/触控区比例;也是 16px 的 1.25 倍。
226
+ --epx-icon-size: 20px;
227
+
228
+ // ② EP 控件抬到触控尺度
229
+ // ⚠️ 高度旋钮是 --el-input-height。打在 .el-input__inner 上会 +2px 且变量停在 32px
230
+ // (已实测,见 commit 03aaca9fc)。
231
+ --el-component-size: var(--epx-touch-min);
232
+ --el-input-height: var(--epx-touch-min);
233
+
234
+ // ⚠️ 已知问题(**未修,等决策**):iOS Safari 对 computed font-size < 16px 的表单控件,
235
+ // 聚焦时会自动放大整页。这里桥接过来是 14px(跟随字号轴),所以移动端点输入框会缩放一下。
236
+ // 改造前(Vant 时期 van-field 也是 14px)就有这个问题,不是本次引入的。
237
+ // 要修的话在本块加一行:--el-font-size-base: max(16px, calc(var(--epx-font-size-base) + 2px));
238
+ // —— 必须写成相对轴的表达式,不能写死常数:上面 html:root 里
239
+ // 是字号轴通到 EP 全组件的路,
240
+ // 本块 (0,2,0) 压得过它 (0,1,1),写死常数会把用户的字号设置掐断。
241
+ // 实测三档:small 13→16 / 默认 14→16 / large 16→18。
242
+ // 按「默认观感等于改造前」的口径,这里保持不改。
243
+ }
244
+
245
+ // ⚠️ 已知问题(**未修,等决策**):移动端次要文字沿用桌面的 #909399,
246
+ // 在白底上对比度只有 2.87:1,而 WCAG AA 正文要求 4.5:1。
247
+ // 不达标的有:单元格取值、单元格副标题、底导未选中项、下拉刷新文案。
248
+ // 实测见 build/.perf/token-audit.mjs。改造前就是这个值,不是本次引入的。
249
+ // 要修的话加一块(**必须排除暗色**,否则会把暗色的 #a3a6ad 一起盖掉、
250
+ // 对比度反而从 6.8 掉到 3.67 —— 已实测过一次):
251
+ // :root[data-viewport=mobile]:not([data-theme=dark]):not(.dark) {
252
+ // --epx-text-secondary: #6b6f7a; // 对白底与页面底都过 4.5,色相保持冷灰
253
+ // }
254
+ // 按「默认观感等于改造前」的口径,这里保持不改。
255
+
256
+ // ── 语义 utility 小集合(刻意克制:只做间距/圆角这类无语义样式,不做 Tailwind 全家桶)──
257
+ @each $i in (1, 2, 3, 4, 5, 6) {
258
+ .epx-p-#{$i} { padding: var(--epx-space-#{$i}) !important; }
259
+ .epx-m-#{$i} { margin: var(--epx-space-#{$i}) !important; }
260
+ .epx-gap-#{$i} { gap: var(--epx-space-#{$i}) !important; }
261
+ }
262
+ .epx-radius { border-radius: var(--epx-radius-base) !important; }
263
+ .epx-radius-l { border-radius: var(--epx-radius-l) !important; }
264
+
265
+ // 每套预设主题的 --color-primary($theme map 来自全局注入的 mixins.scss;custom 由运行时 JS 注入)。
266
+ // 原在 mixins.scss——它被注入每个编译单元会复制 N 份,挪到本文件只编译一次。
267
+ @each $name, $c in $theme {
268
+ @if $name != 'custom' {
269
+ // key(purple/orange…)恰是 CSS 颜色关键字,sass 会解析成颜色字面量——'' + $name 转字符串再插值;
270
+ // red()/green()/blue() 已弃用 → color.channel
271
+ [data-theme=#{'' + $name}] {
272
+ --color-primary: #{color.channel($c, "red", $space: rgb)},#{color.channel($c, "green", $space: rgb)},#{color.channel($c, "blue", $space: rgb)};
273
+ }
274
+ }
275
+ }