agentcss 0.25.4 → 0.25.6
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.
- package/01-tokens/base.reset.css +99 -0
- package/01-tokens/tokens.core.css +954 -0
- package/02-primitives/combobox/combobox.js +3 -0
- package/02-primitives/date-range/date-range.js +10 -1
- package/02-primitives/popover/popover.js +2 -0
- package/03-patterns/drawer/drawer.js +2 -0
- package/04-recipes/app-shell/shell-nav.js +1 -0
- package/09-assets/model-viewer/model-viewer.js +11 -2
- package/09-assets/scientific-plot/scientific-plot.js +1 -0
- package/CHANGELOG.md +141 -0
- package/CONTRIBUTING.md +4 -3
- package/README.md +20 -3
- package/README.zh-CN.md +41 -16
- package/SECURITY.md +28 -9
- package/START-HERE.md +7 -7
- package/SUPPORT.md +2 -2
- package/ai/components.json +1 -1
- package/dist/01-tokens/base.reset.css +1 -0
- package/dist/01-tokens/tokens.core.css +1 -0
- package/dist/02-primitives/combobox/combobox.js +3 -0
- package/dist/02-primitives/date-range/date-range.js +9 -1
- package/dist/02-primitives/popover/popover.js +1 -0
- package/dist/03-patterns/drawer/drawer.js +1 -0
- package/dist/04-recipes/app-shell/shell-nav.js +1 -0
- package/dist/09-assets/model-viewer/model-viewer.js +9 -2
- package/dist/09-assets/scientific-plot/scientific-plot.js +1 -0
- package/dist/COSTS.md +6 -6
- package/docs/ACCESSIBILITY.md +2 -2
- package/docs/CONVENTIONS.md +18 -6
- package/docs/LAYER-BASELINE.md +2 -2
- package/index.html +1 -1
- package/llms-full.txt +1 -1
- package/package.json +17 -5
|
@@ -224,11 +224,13 @@
|
|
|
224
224
|
var q = input.value.trim();
|
|
225
225
|
shown = filter(q).slice(0, 50);
|
|
226
226
|
if (!shown.length) {
|
|
227
|
+
/* @html-sink escaped — 插值只有 q,且过了 esc() */
|
|
227
228
|
list.innerHTML = '<li class="combo__empty" role="presentation">' +
|
|
228
229
|
(q ? '没有匹配「' + esc(q) + '」的选项' : '暂无可选') + '</li>';
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
var ql = q.toLowerCase();
|
|
233
|
+
/* @html-sink escaped — label/value 全部过 esc();list.id / i / picked 是内部量 */
|
|
232
234
|
list.innerHTML = shown.map(function (o, i) {
|
|
233
235
|
var picked = tags.indexOf(o.value) >= 0;
|
|
234
236
|
var label = esc(o.label);
|
|
@@ -253,6 +255,7 @@
|
|
|
253
255
|
/* ---------- 标签 ---------- */
|
|
254
256
|
function paintTags() {
|
|
255
257
|
if (!tagBox) return;
|
|
258
|
+
/* @html-sink escaped — o.label 与 v 都过 esc() */
|
|
256
259
|
tagBox.innerHTML = tags.map(function (v) {
|
|
257
260
|
var o = ALL.filter(function (x) { return x.value === v; })[0] || { label: v };
|
|
258
261
|
/* ⭐ 删除按钮有 aria-label(否则读屏只念"×")*/
|
|
@@ -172,7 +172,15 @@
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function paintSummary(s) {
|
|
175
|
-
|
|
175
|
+
/* 🔴 不拼 innerHTML(E299):`s` 来自两个 <input> 的 value,
|
|
176
|
+
而 `type=date` 在 Chromium 97 旧内核会回退成文本输入 ⇒ value 可为任意串。 */
|
|
177
|
+
if (!summary) return;
|
|
178
|
+
summary.textContent = '';
|
|
179
|
+
if (!s) return;
|
|
180
|
+
summary.appendChild(document.createTextNode('区间 '));
|
|
181
|
+
var b = document.createElement('b');
|
|
182
|
+
b.textContent = s;
|
|
183
|
+
summary.appendChild(b);
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
function emit() {
|
|
@@ -213,6 +221,7 @@
|
|
|
213
221
|
}
|
|
214
222
|
var box = root.querySelector('[data-dr-presets]');
|
|
215
223
|
if (!box) return;
|
|
224
|
+
/* @html-sink internal — 只有固定标签与 p.key(本文件常量) */
|
|
216
225
|
box.innerHTML = presets.map(function (p) {
|
|
217
226
|
return '<button class="drange__preset" type="button" aria-pressed="false" ' +
|
|
218
227
|
'data-dr-preset="' + p.key + '">' + p.label + '</button>';
|
|
@@ -181,6 +181,8 @@
|
|
|
181
181
|
(加了就是模态,语义就错了)。 */
|
|
182
182
|
box.setAttribute('role', 'dialog');
|
|
183
183
|
box.setAttribute('aria-label', opts.label || '');
|
|
184
|
+
/* @html-sink raw-html — `opts.html` 是有意的 HTML 入口,调用方负责净化;
|
|
185
|
+
登记在 SECURITY.md「HTML 入口(sink)」。 */
|
|
184
186
|
box.innerHTML =
|
|
185
187
|
'<span class="popover__arrow" aria-hidden="true"></span>' +
|
|
186
188
|
'<div class="popover__body">' + (opts.html || '') + '</div>';
|
|
@@ -247,6 +247,8 @@
|
|
|
247
247
|
|
|
248
248
|
var body = document.createElement('div');
|
|
249
249
|
body.className = 'drawer__body';
|
|
250
|
+
/* @html-sink raw-html — `opt.content` 是有意的 HTML 入口,调用方负责净化;
|
|
251
|
+
登记在 SECURITY.md「HTML 入口(sink)」。 */
|
|
250
252
|
if (typeof opt.content === 'string') body.innerHTML = opt.content;
|
|
251
253
|
else if (opt.content) body.appendChild(opt.content);
|
|
252
254
|
|
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
btn.className = 'shell__menu';
|
|
176
176
|
btn.setAttribute('aria-label', o.label || ('打开' + title));
|
|
177
177
|
btn.setAttribute('aria-haspopup', 'dialog');
|
|
178
|
+
/* @html-sink internal — ICON 是本文件的固定 SVG 常量,无插值 */
|
|
178
179
|
btn.innerHTML = ICON;
|
|
179
180
|
btn.addEventListener('click', function () { self.open(); });
|
|
180
181
|
topbar.insertBefore(btn, topbar.firstChild);
|
|
@@ -116,8 +116,16 @@
|
|
|
116
116
|
this.ui.appendChild(this.btn);
|
|
117
117
|
|
|
118
118
|
this.hint = el('p', 'mv__hint');
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
/* 🔴 文件名来自 `src`,可能是调用方拼出来的 URL(E299)。
|
|
120
|
+
原来用 innerHTML 直接拼 ⇒ `<img onerror>` 这类值会被解析成节点。
|
|
121
|
+
改成 textContent 组装:`<code>` 与 `<b>` 仍然存在,但文本永远只是文本。 */
|
|
122
|
+
var fname = el('code', null, this.src.split('/').pop() || '—');
|
|
123
|
+
var strong = el('b', null, '点击后才下载');
|
|
124
|
+
this.hint.appendChild(document.createTextNode('模型 '));
|
|
125
|
+
this.hint.appendChild(fname);
|
|
126
|
+
this.hint.appendChild(document.createTextNode(' 会'));
|
|
127
|
+
this.hint.appendChild(strong);
|
|
128
|
+
this.hint.appendChild(document.createTextNode(',首屏不占流量。'));
|
|
121
129
|
this.ui.appendChild(this.hint);
|
|
122
130
|
|
|
123
131
|
if (!hasWebGL()) {
|
|
@@ -351,6 +359,7 @@
|
|
|
351
359
|
self.ui.innerHTML = '';
|
|
352
360
|
var cap = el('p', 'mv__cap');
|
|
353
361
|
var dt = Math.round(((window.performance && performance.now) ? performance.now() : Date.now()) - t0);
|
|
362
|
+
/* @html-sink internal — 插值只有 fmtKB() 的返回值与换算后的毫秒数 */
|
|
354
363
|
cap.innerHTML = '模型 <b>' + fmtKB(bytes || self.lastBytes || 0) + '</b> · 引擎就绪耗时 <b>' + dt + 'ms</b>' +
|
|
355
364
|
(controls ? ' · 拖动可旋转,滚轮/双指可缩放' : ' · 未加载控制器,无法拖动');
|
|
356
365
|
self.node.appendChild(cap);
|
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,147 @@
|
|
|
16
16
|
|
|
17
17
|
(无)
|
|
18
18
|
|
|
19
|
+
## [0.25.6] - 2026-11-04
|
|
20
|
+
|
|
21
|
+
这一版**没有改组件、令牌、CSS 或运行时 JS**。改的是「装到手里的包可不可信」:
|
|
22
|
+
文档链接、依赖来源、包里不该有的临时文件,以及几道"报告通过却什么都没检查"
|
|
23
|
+
的发布门禁。从 `0.25.5` 升级不需要改代码。
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- 依赖锁文件里有一处 `yaml` 仍指向第三方镜像 `registry.npmmirror.com`,
|
|
28
|
+
而 `npm ci` 按 `resolved` 直取,会绕开仓库钉住的官方源。已改回
|
|
29
|
+
`registry.npmjs.org`,并核对官方 tarball 的 sha512 与锁文件 `integrity` 一致。
|
|
30
|
+
新增 `lockfile-source-gate`:每个 `resolved` 的 host 都要在白名单里,
|
|
31
|
+
镜像源、缺 integrity、非 https 都会红。
|
|
32
|
+
- `package.json` 的 `files` 排除项补上临时文件后缀与门禁探针名。
|
|
33
|
+
之前反向控制留在源码目录里的 `.bak` 备份会被目录级白名单一起收进包;
|
|
34
|
+
实测加排除前探针进包、加后不进。
|
|
35
|
+
- HTML 注入点的盘点门禁原本只扫五个目录,现在扫描面由 `files` 白名单推导,
|
|
36
|
+
新增目录自动纳入;已复核 `esm/`、`adapters/` 里没有注入点。
|
|
37
|
+
两处把 `src` / `value` 拼进 `innerHTML` 的组件改成 DOM 节点组装。
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- 包内四份文档(README、README.zh-CN、SUPPORT、docs/LAYER-BASELINE)里
|
|
42
|
+
有八个相对链接指向仓库文件,而这些文件不在发布包里 —— 装了包的人点开就是死链。
|
|
43
|
+
改为指向公开仓库的同一份文件;新增包视角链接门禁,用
|
|
44
|
+
`npm pack --dry-run --json` 的真实清单逐条验,不再只从仓库视角看。
|
|
45
|
+
- 打印门禁在并发下偶发误报:活标本 iframe 的样式表已挂上、框高却还是初始值,
|
|
46
|
+
打印版面被裁掉一半,判据把环境抖动报成内容回归。现在要求文档、样式、字体
|
|
47
|
+
都就绪且**框高装得下内容**,并在切到打印媒体后重新判定。
|
|
48
|
+
- 文档事实门禁抓到 `CONTRIBUTING.md` 写死了门禁条数;改为让条数只由门禁实算。
|
|
49
|
+
`package.json` 里三条 scripts 缩进错乱,以及 README 里几处历史措辞,一并修正。
|
|
50
|
+
- `release-gate` 曾把新加的通配排除判成「指向不存在的东西」;
|
|
51
|
+
现在字面排除继续查存在,glob 排除只分类、不按当前有没有文件判。
|
|
52
|
+
- 一条写盘的反向控制漏标 `@serial`,与 `dist-fresh` 并发时报出「孤儿产物」。
|
|
53
|
+
已补标记,并把那个临时探针也挡在包外。
|
|
54
|
+
|
|
55
|
+
### Changed · 门禁自证(不进 npm 包)
|
|
56
|
+
|
|
57
|
+
这些是仓库里的门禁与内部台账,装包的人拿不到,但决定了「全绿」值不值得信:
|
|
58
|
+
|
|
59
|
+
- `doc-imports.js` 原来读**仓库**的文档、却在**消费者**侧解析,验错了对象;
|
|
60
|
+
而且一份文档都没扫到、或抽出来的全是占位符时照样通过。改为读已安装包里的
|
|
61
|
+
文档,补零视野守卫、显式承诺清单与显式豁免(`CHANGELOG.md` 里记着历史坏路径,
|
|
62
|
+
不能当成给读者照抄的入口)、未登记文档自动报红,并补了反向控制。
|
|
63
|
+
实测:往包内 README 注入一条坏路径,旧判据绿、新判据红。
|
|
64
|
+
- `entry-resolve.js` 在 `exports` 不产生任何说明符时打印 `0/0` 却返回通过,
|
|
65
|
+
还输出「两套条件都能解析到真实文件」这句假结论。现在零输入即红,
|
|
66
|
+
并补了覆盖传递闭包、import 条件、通配展开的反向控制。
|
|
67
|
+
- `pack-smoke.sh` 把门禁输出用 `tail` 截成两行,扫描规模那行看不见了;
|
|
68
|
+
现在保留统计行,看到 0 就能怀疑。
|
|
69
|
+
- 内部路线图与待办台账里几处仍写着「尚未发版 / 待远端验证」的活跃状态,
|
|
70
|
+
按真值更正,历史段落只加注记、不改写。
|
|
71
|
+
|
|
72
|
+
上述每一项都跑了正向、反向控制与真实发布包验证;发布前全量门禁 0 FAIL。
|
|
73
|
+
新增的失误与纠正规则登记在内部 `ERRORS.md`(不随包分发)。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## [0.25.5] - 2026-11-04
|
|
78
|
+
|
|
79
|
+
### 新增:`tokens.core.css`(只有变量)与 `base.reset.css`(只有底座)
|
|
80
|
+
|
|
81
|
+
`tokens.css` 一直同时干两件事:定义变量,以及**改整个宿主文档**的三条规则
|
|
82
|
+
(全局 `:focus-visible`、`a/button/label…` 的 `-webkit-tap-highlight-color`、
|
|
83
|
+
`prefers-reduced-motion` 里对 `*` 的 `!important`)。README 的最小示例却把这套组合
|
|
84
|
+
描述成"零副作用"——嵌进已有站点的人会发现宿主的焦点环与点按反馈被换掉了,
|
|
85
|
+
而看不出是哪一行。
|
|
86
|
+
|
|
87
|
+
冻结期不能改 `tokens.css` 的语义,所以只做加法:
|
|
88
|
+
|
|
89
|
+
| 新入口 | 含什么 | 改宿主吗 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `agentcss/tokens.core.css` | 只有变量(含暗色、按语言的字体与字距) | 不会 |
|
|
92
|
+
| `agentcss/base.reset.css` | 只有那三条 | 会 |
|
|
93
|
+
| `agentcss/tokens.css` | 旧路,一个字节没动 | 会(同上) |
|
|
94
|
+
|
|
95
|
+
两个新文件是**生成物**(`05-audit/split-tokens.py` 从 `tokens.css` 切出来),
|
|
96
|
+
`token-split` 门禁逐字节比对 —— 同一事实不留第二份手写来源(I-7)。
|
|
97
|
+
|
|
98
|
+
**计算样式实测**(light / dark 各一次,同一页面只换引入文件):
|
|
99
|
+
|
|
100
|
+
| | `--paper` | 焦点环 | 点按高亮 |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| core | `#f6f7f8` / `#14171A` | `auto 1px`(浏览器默认) | 保留 `0.18` |
|
|
103
|
+
| tokens.css | `#f6f7f8` / `#14171A` | `solid 2px`(库的) | 被关掉 `0` |
|
|
104
|
+
|
|
105
|
+
判据 5 条 + 反向控制 4 条。核心一条:**每条顶层规则必须归入 core / reset / library**,
|
|
106
|
+
归不了类就红 —— 将来有人往 `tokens.css` 加第四条全局规则,会被逼着做一次显式决定。
|
|
107
|
+
|
|
108
|
+
过程中自己踩的两个坑,都写进了判据:
|
|
109
|
+
- 按选择器前缀归类 ⇒ 暗色令牌在 `@media screen and (prefers-color-scheme: dark)` 里,
|
|
110
|
+
被判成"库自己的东西",core 里没有暗色;**计算样式实测 dark 下仍返回 `#f6f7f8`**。
|
|
111
|
+
改成递归判"块里是不是只有自定义属性"。
|
|
112
|
+
- `_inner()` 在原文里找第一个 `{`,而块首注释里就有 `{`(举例 `.wrap { max-width }`)
|
|
113
|
+
⇒ 切到注释中间,后面全错。索引必须从遮蔽层取。
|
|
114
|
+
|
|
115
|
+
### 新增:每个 HTML 注入点都要说得清装的是什么(`xss-sink-gate`)
|
|
116
|
+
|
|
117
|
+
全库 21 处 `.innerHTML` / `.insertAdjacentHTML`,此前没有任何机制阻止第 22 处悄悄进来,
|
|
118
|
+
`SECURITY.md` 只写了 `combobox` 一处、并称"没有门禁守着"。现在分四类
|
|
119
|
+
(清空 12 / 已转义 3 / 内部量 4 / 有意入口 2),非"赋空串"的必须在紧邻上一行写
|
|
120
|
+
`/* @html-sink <kind> — <理由> */`。
|
|
121
|
+
|
|
122
|
+
两处**真缺陷**已修:
|
|
123
|
+
- `model-viewer` 把 `src` 的文件名拼进 `innerHTML`。文件名可以是
|
|
124
|
+
`<img src=x onerror=…>`。改成 `textContent` 组装。载荷实测:注入串只以
|
|
125
|
+
`<img` 出现,`injectedImg=false`、`flag=false`。
|
|
126
|
+
- `date-range` 把 `<input>` 的 `value` 拼进 `innerHTML`,而 `type=date` 在
|
|
127
|
+
Chromium 97 旧内核会回退成文本输入 ⇒ `value` 可为任意串。改成节点组装(摘要文本不变,
|
|
128
|
+
组件契约门禁仍断言「区间 2026-01-01 → 2026-01-31(31 天)」)。
|
|
129
|
+
|
|
130
|
+
`raw-html` 两处(`popover` 的 `opts.html`、`drawer` 的 `opt.content`)登记在
|
|
131
|
+
`SECURITY.md` 的「HTML 入口」,与代码**双向**核对(陈旧登记也红)。
|
|
132
|
+
|
|
133
|
+
**这道门禁证明不了什么**:它不证明 `esc()` 的实现对,也不覆盖全部数据流。
|
|
134
|
+
它证明的是"新增未声明的注入点必须显式改标记或改文档"。
|
|
135
|
+
|
|
136
|
+
### 修正:文档里三处正在说谎的地方
|
|
137
|
+
|
|
138
|
+
- `CONVENTIONS.md` 写「`!important` 现有 10 处(无障碍 9 + `[hidden]` 1)」,
|
|
139
|
+
门禁实测 **13 处**:多出来的 2 处是 `@media print` —— 文档连**类别**都没列。
|
|
140
|
+
按文档去审计的人会把这两处正当的写成"说不清为什么要赢"。改成类别表 + 「要数字跑门禁」。
|
|
141
|
+
- `README` 说「`components.json` 里标 `beta` 的那几个组件」,实际 **40/40 stable**。
|
|
142
|
+
改成讲清四条机械判据,并写明 `stable` 量的是机制完备度、不是"久经生产"。
|
|
143
|
+
- `README` 安装段还留着「发布成功之后这一行才会换上」的历史段落(上一句已写已发布);
|
|
144
|
+
源码复制那段编号从 ② 改 ③。
|
|
145
|
+
|
|
146
|
+
### 体积账本:去掉重复计数,不放宽上限
|
|
147
|
+
|
|
148
|
+
`tokens.core.css` / `base.reset.css` 是 `tokens.css` 的切片,消费者只下载其中一个。
|
|
149
|
+
三个文件一起计入会把同一批声明算三遍 ⇒ `perf-gate` 排除这两个**已知切片**并在输出里
|
|
150
|
+
打印说明;`ceiling` / `granted` 一个都没动。
|
|
151
|
+
|
|
152
|
+
### 验证
|
|
153
|
+
|
|
154
|
+
- 快速层 **120 道 / 0 FAIL**;打包冒烟(含消费者侧文档路径解析)通过。
|
|
155
|
+
- 视觉基线重录,但 **109 张 PNG 逐字节未变**,只有 `_stamp.json` 更新。
|
|
156
|
+
- 反向控制:`xss-sink` 12 条、`token-split` 4 条,每条都证明"该红时真的红"。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
19
160
|
## [0.25.4] - 2026-11-03
|
|
20
161
|
|
|
21
162
|
**修订版:把「类型里看不见的那 8 个全局」补上,并让这件事以后有人管。
|
package/CONTRIBUTING.md
CHANGED
|
@@ -29,9 +29,10 @@ npm run gate # 全量:含浏览器类
|
|
|
29
29
|
npm run gate:fast # 只跑静态层,不用装浏览器
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
全量的门禁条数由 `05-audit/check-all.sh` 实算,**不在文档里写死** —— 新增一道门禁
|
|
33
|
+
就会变,写下来的第二天就是第二个会漂的真值(I-7)。默认 `min(核数, 6)` 路并发;
|
|
34
|
+
组件层要起浏览器,还要逐张比浅色 / 暗色 / RTL 三套视觉基线(按平台分集,
|
|
35
|
+
只比本平台那套)。条数、耗时、并发效率一律以**本轮门禁打印的结果**为准。
|
|
35
36
|
只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
|
|
36
37
|
|
|
37
38
|
### 全量为什么**不**在 CI 上跑(这不是缺口,是结论)
|
package/README.md
CHANGED
|
@@ -75,12 +75,29 @@ npm install github:owain323/frontend-lib
|
|
|
75
75
|
|
|
76
76
|
**1 · Bring in the styles** (with your bundler)
|
|
77
77
|
|
|
78
|
+
First decide whether you want the **base layer**. It is the only part of this library that touches the host page.
|
|
79
|
+
|
|
80
|
+
| File | Contains | Touches the host? |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `agentcss/tokens.core.css` | CSS custom properties only (incl. dark and per-language overrides) | **No** |
|
|
83
|
+
| `agentcss/base.reset.css` | Global focus ring, `a/button/label…` tap highlight, reduced-motion | Yes (three rules) |
|
|
84
|
+
| `agentcss/tokens.css` | Both of the above combined (the old path, byte-for-byte unchanged) | Yes (same three) |
|
|
85
|
+
|
|
78
86
|
```js
|
|
87
|
+
// Dropping into an existing site: variables only, host focus ring and tap feedback untouched
|
|
88
|
+
import 'agentcss/tokens.core.css';
|
|
89
|
+
|
|
90
|
+
// Building a page from scratch: one import (equivalent to tokens.core.css + base.reset.css)
|
|
79
91
|
import 'agentcss/tokens.css';
|
|
92
|
+
|
|
80
93
|
import 'agentcss/typography.css';
|
|
81
94
|
import 'agentcss/primitives/button/button.css';
|
|
82
95
|
```
|
|
83
96
|
|
|
97
|
+
The three base-layer rules, spelled out: a global `:focus-visible` ring; `-webkit-tap-highlight-color: transparent` on bare links/buttons/labels; and a `prefers-reduced-motion` block that overrides `*, *::before, *::after` with `!important`.
|
|
98
|
+
|
|
99
|
+
> Both files are **generated** from `01-tokens/tokens.css` by `05-audit/split-tokens.py` and checked byte-for-byte by a gate. Edit the token source, then run `python 05-audit/split-tokens.py --write`.
|
|
100
|
+
|
|
84
101
|
**2 · For interactive components, load the behaviour script**
|
|
85
102
|
|
|
86
103
|
Behaviour scripts are IIFEs that attach to `window` (e.g. `window.Overlay`).
|
|
@@ -252,7 +269,7 @@ Two builds ship:
|
|
|
252
269
|
The source tree keeps every comment — that is documentation, not payload.
|
|
253
270
|
|
|
254
271
|
**One frozen ceiling.** The whole library is held to a single budget recorded in
|
|
255
|
-
[`05-audit/budget.json`](05-audit/budget.json): a hard ceiling, a reserve that absorbs
|
|
272
|
+
[`05-audit/budget.json`](https://github.com/owain323/frontend-lib/blob/master/05-audit/budget.json): a hard ceiling, a reserve that absorbs
|
|
256
273
|
normal additions *without anyone editing a number*, and a ratchet that tightens the
|
|
257
274
|
budget when the library shrinks. The rules — and what would have to be true to raise
|
|
258
275
|
the ceiling — are in [`docs/BUDGET.md`](docs/BUDGET.md).
|
|
@@ -273,7 +290,7 @@ not — is in [`docs/STABILITY.md`](docs/STABILITY.md).
|
|
|
273
290
|
- **Tested once** (`🔍`) means exactly that: one manual run on a real device, with a full UA and a per-capability result, but **no automated check watching it**. A browser update will not turn anything red. It never counts as verified, and the two never add up.
|
|
274
291
|
- **Not claimed** means exactly that. A row that has not been tested is not written as supported.
|
|
275
292
|
|
|
276
|
-
Mobile WebView support is where that distinction bites. Seven entry points have been probed by hand — per-capability results in [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md) — while Android System WebView still has **no data at all**. None of the seven was checked for soft-keyboard, safe-area, scroll-bounce or 200 % text behaviour: "15/15 capabilities" is not "the page works on a phone".
|
|
293
|
+
Mobile WebView support is where that distinction bites. Seven entry points have been probed by hand — per-capability results in [`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md) — while Android System WebView still has **no data at all**. None of the seven was checked for soft-keyboard, safe-area, scroll-bounce or 200 % text behaviour: "15/15 capabilities" is not "the page works on a phone".
|
|
277
294
|
|
|
278
295
|
## Theming
|
|
279
296
|
|
|
@@ -291,7 +308,7 @@ Colour comes from tokens only. Component CSS never writes `@media (prefers-color
|
|
|
291
308
|
|
|
292
309
|
## Accessibility
|
|
293
310
|
|
|
294
|
-
Every component carries a contract covering focus order, keyboard operation, ARIA state and contrast; the checks run in real browsers rather than only reading source text.
|
|
311
|
+
Every component carries a contract covering focus order, keyboard operation, ARIA state and contrast; the checks run in real browsers rather than only reading source text. Maturity is computed from four mechanical criteria (demo + dedicated gate + state on `data-*` + zero behaviour debt), not assigned by hand. All 40 currently pass, so there is no `beta` component — that measures **mechanism completeness**, not how many production projects have shipped it.
|
|
295
312
|
|
|
296
313
|
## Development
|
|
297
314
|
|
package/README.zh-CN.md
CHANGED
|
@@ -74,15 +74,10 @@ npm install github:owain323/frontend-lib
|
|
|
74
74
|
> registry 上没有可安装版本,靠 GitHub Release 附件分发(当时的分发路径记在
|
|
75
75
|
> `CHANGELOG.md` 的 0.11.2 一节)。自 0.11.2 起这条通路已恢复。
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
**③ 源码复制**(不想装依赖):把 `01-tokens/` 和需要的组件目录复制进你的项目。
|
|
78
78
|
⚠️ 复制 `dist/` 里的版本,**别复制源码版** —— 源码版有一半以上是给人和 AI 读的注释
|
|
79
79
|
(理由见下面「体积」一节)。
|
|
80
80
|
|
|
81
|
-
> 为什么这里不提前写 `npm install agentcss`:
|
|
82
|
-
> 写了而 registry 上还没有,照做的人会拿到 404,然后以为是自己的环境问题 ——
|
|
83
|
-
> 这与本库曾经踩过的「README 教人装、装到的是别人 2017 年的包」是同一类坑。
|
|
84
|
-
> 发布成功之后这一行才会换上。
|
|
85
|
-
|
|
86
81
|
## 组件一览
|
|
87
82
|
|
|
88
83
|
共 **40** 个(stable 40 · 全部通过四条判据)。每个组件目录下有 `README.md`(为什么这么做)和 `demo.html`(能直接抄的结构)。
|
|
@@ -152,12 +147,37 @@ npm install github:owain323/frontend-lib
|
|
|
152
147
|
|
|
153
148
|
**1 · 引入样式**(用你的打包器)
|
|
154
149
|
|
|
150
|
+
先做一个决定:**要不要底座层**。这是全库唯一会改到宿主页面的地方。
|
|
151
|
+
|
|
152
|
+
| 文件 | 含什么 | 会改宿主吗 |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `agentcss/tokens.core.css` | 只有 CSS 变量(含暗色、按语言的字体与字距覆盖) | **不会** |
|
|
155
|
+
| `agentcss/base.reset.css` | 全局焦点环、`a/button/label…` 的点按高亮、减少动效 | 会(三条,见下) |
|
|
156
|
+
| `agentcss/tokens.css` | 上面两者的合集(旧路,行为一个字节都没变) | 会(同上三条) |
|
|
157
|
+
|
|
155
158
|
```js
|
|
159
|
+
// 嵌进已有站点:只要变量,不动宿主的焦点环与点按反馈
|
|
160
|
+
import 'agentcss/tokens.core.css';
|
|
161
|
+
|
|
162
|
+
// 从零搭页面:一条搞定(等价于 tokens.core.css + base.reset.css)
|
|
156
163
|
import 'agentcss/tokens.css';
|
|
164
|
+
|
|
157
165
|
import 'agentcss/typography.css';
|
|
158
166
|
import 'agentcss/primitives/button/button.css';
|
|
159
167
|
```
|
|
160
168
|
|
|
169
|
+
**底座层具体改什么**(这三条作用于宿主,不只是本库组件):
|
|
170
|
+
|
|
171
|
+
1. `:focus-visible` —— 所有可聚焦元素拿到本库的焦点环(2px `--accent`);
|
|
172
|
+
2. `a, button, label, summary, input[type=checkbox]…` —— 原生点按高亮被关掉
|
|
173
|
+
(`-webkit-tap-highlight-color: transparent`);
|
|
174
|
+
3. `@media (prefers-reduced-motion: reduce)` —— 对 `*, *::before, *::after`
|
|
175
|
+
压掉动画与过渡(带 `!important`)。
|
|
176
|
+
|
|
177
|
+
> 这两个文件是**生成物**:由 `05-audit/split-tokens.py` 从 `tokens.css` 切出来,
|
|
178
|
+
> 门禁逐字节核对。想改令牌只有一个地方 —— `01-tokens/tokens.css`,
|
|
179
|
+
> 改完跑 `python 05-audit/split-tokens.py --write`。
|
|
180
|
+
|
|
161
181
|
**2 · 需要交互组件时**,引入行为脚本
|
|
162
182
|
|
|
163
183
|
行为脚本是 IIFE,挂到 `window` 上(如 `window.Overlay`)。
|
|
@@ -185,11 +205,11 @@ import 'agentcss/patterns/overlay/overlay.js';
|
|
|
185
205
|
|
|
186
206
|
```
|
|
187
207
|
dist/01-tokens/tokens.css ← 拷这个
|
|
188
|
-
01-tokens/tokens.css ←
|
|
208
|
+
01-tokens/tokens.css ← 源码(带全部注释),用于阅读
|
|
189
209
|
```
|
|
190
210
|
|
|
191
|
-
- **别拷源码目录**除非你要读它:全库 CSS
|
|
192
|
-
|
|
211
|
+
- **别拷源码目录**除非你要读它:全库 CSS 里注释占了多数字节(精确比例见
|
|
212
|
+
`dist/COSTS.md` —— 那里是现算的,不手抄),`page.css` 单个尤其明显。
|
|
193
213
|
- **JS 也拷 `dist/`**:那一份去了注释但没压缩(理由见「体积」一节)。
|
|
194
214
|
|
|
195
215
|
---
|
|
@@ -256,12 +276,13 @@ dist/01-tokens/tokens.css ← 拷这个
|
|
|
256
276
|
## 体积:默认给你的已经是干净产物
|
|
257
277
|
|
|
258
278
|
源码目录(`01-tokens/` `02-primitives/` …)里的 CSS **带着全部注释** ——
|
|
259
|
-
|
|
279
|
+
注释占了多数字节(精确体积由 `dist/COSTS.md` 现算,这里是故意不手抄的),
|
|
280
|
+
`page.css` 单个更高。
|
|
260
281
|
|
|
261
282
|
那些注释是写给你看的取舍理由(为什么这么定、踩过什么坑),不是写给你浏览器看的。
|
|
262
|
-
|
|
283
|
+
**注释让你看代码时处处撞墙;从源码目录引入的话,构建工具还会把那部分字节原样打进产物。**
|
|
263
284
|
|
|
264
|
-
**一个冻结的上限。** 整套库受一份账本约束([`05-audit/budget.json`](05-audit/budget.json)):
|
|
285
|
+
**一个冻结的上限。** 整套库受一份账本约束([`05-audit/budget.json`](https://github.com/owain323/frontend-lib/blob/master/05-audit/budget.json)):
|
|
265
286
|
硬天花板 + 一份「常规新增组件不用改任何数字」的储备 + 一个只在体积下降时收紧的棘轮。
|
|
266
287
|
规则、以及「什么情况才允许抬天花板」,在 [`docs/BUDGET.md`](docs/BUDGET.md)。
|
|
267
288
|
|
|
@@ -435,7 +456,7 @@ bash 05-audit/check-all.sh # 全量门禁约 130 项,本机约 8 分钟
|
|
|
435
456
|
`🔍` 是「在真实设备上人工试过一次,之后没人再看」——
|
|
436
457
|
浏览器升级只会被前者发现。逐项结果、完整 UA 与复核方法见
|
|
437
458
|
[`docs/LAYER-BASELINE.md`](docs/LAYER-BASELINE.md) 与
|
|
438
|
-
[`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md)。
|
|
459
|
+
[`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md)。
|
|
439
460
|
|
|
440
461
|
⚠️ 定位里写「移动 Web」指的是**设计取向**(触控优先、命中区不小于 44px),
|
|
441
462
|
不代表已被自动化覆盖,更不代表页面在真机上就能用:
|
|
@@ -528,9 +549,13 @@ npm run benchmark # 兼容性基准
|
|
|
528
549
|
|
|
529
550
|
改代码时跑 `gate:fast` 即可;**提交前**再跑一次 `gate`。
|
|
530
551
|
|
|
531
|
-
类型声明在 `types/index.d.ts`。改动它之前先看 `API.md`
|
|
532
|
-
|
|
533
|
-
|
|
552
|
+
类型声明在 `types/index.d.ts`。改动它之前先看 `API.md` 的「稳定程度约定」。
|
|
553
|
+
|
|
554
|
+
成熟度不是人写的,是 `ai/components.json` 里**四条机械判据**算出来的
|
|
555
|
+
(有 demo + 有专属门禁 + 状态全在 `data-*` 上 + 行为零欠账)。
|
|
556
|
+
当前 40 个全部满足,所以**没有 beta 组件** —— 但这四条量的是**机制完备度**,
|
|
557
|
+
不是"在多少个生产项目里跑过"。把 `stable` 读成"久经生产"是误读;
|
|
558
|
+
真实采用证据在 `SUPPORT.md` 与 `docs/LAYER-BASELINE.md` 里,按行查。
|
|
534
559
|
|
|
535
560
|
## 许可
|
|
536
561
|
|
package/SECURITY.md
CHANGED
|
@@ -25,20 +25,39 @@
|
|
|
25
25
|
- **构建期依赖**:`package-lock.json` 里是 devDependencies(typescript、puppeteer-core、
|
|
26
26
|
axe-core 等),只在开发和跑门禁时用。它们**不会被使用者安装**。
|
|
27
27
|
- **真实面**:
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
- **HTML 注入点 —— 现在有门禁守着**。全库共 21 处 `.innerHTML` /
|
|
29
|
+
`.insertAdjacentHTML`,按"装的是什么"分四类:清空型(赋空串)、
|
|
30
|
+
escaped(插值过 `esc()`)、internal(只有常量/内部量)、raw-html(有意的
|
|
31
|
+
HTML 入口)。每个非清空型注入点都必须在紧邻上一行写
|
|
32
|
+
`/* @html-sink <kind> — <理由> */`,否则 `xss-sink-gate.py` 红。
|
|
33
|
+
⇒ 这不能证明 `esc()` 一定对,但它能保证:**新增一个未声明的注入点,
|
|
34
|
+
必须显式改标记或改文档**,不会静默进来。
|
|
35
35
|
- **CSS 注入** —— 把不可信内容插进 `class` 属性时同理(本库不读 `class` 里的值,
|
|
36
36
|
所以这不是可利用路径,只是习惯问题)。
|
|
37
37
|
- **DOM 监听泄漏** —— 组件反复挂载/卸载时可能累积监听(部分组件的 `destroy()`
|
|
38
38
|
正在补齐中,见 CHANGELOG)
|
|
39
39
|
|
|
40
|
-
⇒
|
|
41
|
-
|
|
40
|
+
⇒ 也就是说:**本库不太可能成为供应链攻击的一环**。
|
|
41
|
+
|
|
42
|
+
## HTML 入口(sink)
|
|
43
|
+
|
|
44
|
+
下面两处会把字符串**当 HTML 解析**。它们是**有意的 API**,不是疏漏 ——
|
|
45
|
+
`popover` 要能放任意块级内容,`drawer` 要能放整页内容,只收文本就废了。
|
|
46
|
+
|
|
47
|
+
| 文件 | 入口 | 谁负责净化 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `02-primitives/popover/popover.js` | `opts.html` | 调用方 |
|
|
50
|
+
| `03-patterns/drawer/drawer.js` | `opt.content` | 调用方 |
|
|
51
|
+
|
|
52
|
+
**为什么本库不做净化**:净化需要一个 HTML 解析器或一份允许列表实现,而本库的目标是
|
|
53
|
+
零运行时依赖 + Chromium 97 可用。给一个"看起来安全"的半吊子净化,比明确说
|
|
54
|
+
"这是调用方的责任"更危险。
|
|
55
|
+
|
|
56
|
+
调用方该怎么做:内容来自用户输入时,先净化(如 DOMPurify)再传;只想放纯文本时,
|
|
57
|
+
用 `textContent` 或先转义,不要走这两个参数。
|
|
58
|
+
|
|
59
|
+
> 这张表与代码**双向**核对:登记了却没有对应注入点 ⇒ 红(防陈旧登记);
|
|
60
|
+
> 有 raw-html 注入点却没登记 ⇒ 红。判据在 `05-audit/xss-sink-gate.py`。
|
|
42
61
|
|
|
43
62
|
## 支持的版本
|
|
44
63
|
|
package/START-HERE.md
CHANGED
|
@@ -40,20 +40,20 @@
|
|
|
40
40
|
| 卡片 | `02-primitives/card/card.css` | 283 | — | 有分组内容就要 |
|
|
41
41
|
| 单选 / 多选 | `02-primitives/choice/choice.css` | 434 | — | 单选 / 多选就要 |
|
|
42
42
|
| 下拉选择 | `02-primitives/select/select.css` | 151 | **必需 631** (`select.js`) | 原生 select 的外观或交互不够用就要 |
|
|
43
|
-
| 可搜索选择 | `02-primitives/combobox/combobox.css` | 223 | **必需
|
|
43
|
+
| 可搜索选择 | `02-primitives/combobox/combobox.css` | 223 | **必需 480** (`combobox.js`) | 选项多到要搜索,或要选多个并留成标签就要 |
|
|
44
44
|
| 开关 | `02-primitives/switch/switch.css` | 429 | — | 有"立即生效的设置"就要 |
|
|
45
45
|
| 分隔线 | `02-primitives/separator/separator.css` | 73 | — | 要分开内容块就要 |
|
|
46
46
|
| 加载骨架 | `02-primitives/skeleton/skeleton.css` | 114 | — | 数据还没到、先把版式占住就要 |
|
|
47
47
|
| 进度条 / 环 | `02-primitives/progress/progress.css` | 228 | — | 有耗时过程要给进度就要 |
|
|
48
|
-
| 浮层气泡 | `02-primitives/popover/popover.css` | 187 | **必需
|
|
49
|
-
| 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需
|
|
48
|
+
| 浮层气泡 | `02-primitives/popover/popover.css` | 187 | **必需 325** (`popover.js`) | 要一段信息贴着触发元素浮出来就要 |
|
|
49
|
+
| 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 310** (`date-range.js`) | 要选起止日期就要 |
|
|
50
50
|
| 标签页 | `03-patterns/tabs/tabs.css` | 172 | **必需 316** (`tabs.js`) | 同层级内容切换就要 |
|
|
51
51
|
| 折叠面板 | `03-patterns/accordion/accordion.css` | 453 | **必需 389** (`accordion.js`) | 长表单分段就要 |
|
|
52
52
|
| 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 555** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
|
|
53
53
|
| 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 284** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
|
|
54
54
|
| 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
|
|
55
55
|
| 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
|
|
56
|
-
| 侧边抽屉 | `03-patterns/drawer/drawer.css` | 145 | **必需
|
|
56
|
+
| 侧边抽屉 | `03-patterns/drawer/drawer.css` | 145 | **必需 378** (`drawer.js`) | 要从侧边滑出面板就要 |
|
|
57
57
|
| 弹窗 / 吐司 | `03-patterns/overlay/overlay.css` | 338 | **必需 697** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
|
|
58
58
|
| 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 269 | — | **凡是会异步取数就要** |
|
|
59
59
|
| 表单校验 | `03-patterns/form-validation/form-validation.css` | 274 | — | 提交前要校验就要 |
|
|
@@ -66,13 +66,13 @@
|
|
|
66
66
|
| upload | `02-primitives/upload/upload.css` | 490 | 可选 676 (`upload.js`,拖放 / 文件列表 / 校验;没有它基础形态照常工作) | |
|
|
67
67
|
| reveal | `03-patterns/reveal/reveal.css` | 93 | 可选 175 (`reveal.js`,入场动效,纯增强;没有它内容直接可见) | |
|
|
68
68
|
| analysis-report | `04-recipes/analysis-report/analysis-report.css` | 226 | — | |
|
|
69
|
-
| app-shell | `04-recipes/app-shell/app-shell.css` | 588 | **必需
|
|
69
|
+
| app-shell | `04-recipes/app-shell/app-shell.css` | 588 | **必需 289** (`shell-nav.js`) | |
|
|
70
70
|
| detail-page | `04-recipes/detail-page/detail-page.css` | 190 | — | |
|
|
71
71
|
| report | `04-recipes/report/report.css` | 369 | — | |
|
|
72
72
|
| table | `04-recipes/table/table.css` | 409 | — | |
|
|
73
73
|
| bar | `09-assets/bar/bar.css` | 181 | **必需 324** (`bar.js`) | |
|
|
74
|
-
| model-viewer | `09-assets/model-viewer/model-viewer.css` | 143 | **必需
|
|
75
|
-
| scientific-plot | `09-assets/scientific-plot/scientific-plot.css` | 141 | **必需
|
|
74
|
+
| model-viewer | `09-assets/model-viewer/model-viewer.css` | 143 | **必需 432** (`model-viewer.js`) | |
|
|
75
|
+
| scientific-plot | `09-assets/scientific-plot/scientific-plot.css` | 141 | **必需 604** (`scientific-plot.js`) | |
|
|
76
76
|
| sparkline | `09-assets/sparkline/sparkline.css` | 158 | **必需 508** (`sparkline.js`) | |
|
|
77
77
|
|
|
78
78
|
### 🔴 这些组件的 JS 是**必需的**(不是增强)
|
package/SUPPORT.md
CHANGED
|
@@ -97,9 +97,9 @@ grep 一条都数不到),还有 `skip "…"`(这台机器跑不了的门
|
|
|
97
97
|
|
|
98
98
|
### 试过一次的(🔍 人工实测,无回归保护)
|
|
99
99
|
|
|
100
|
-
7 行数据来自在真实设备上打开 [`capability-probe.html`](10-review/webview/capability-probe.html)。
|
|
100
|
+
7 行数据来自在真实设备上打开 [`capability-probe.html`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/capability-probe.html)。
|
|
101
101
|
完整 UA 与逐项结论在 [`docs/LAYER-BASELINE.md`](docs/LAYER-BASELINE.md);
|
|
102
|
-
15 项能力分别是什么、怎么自己复跑一遍,在 [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md)。
|
|
102
|
+
15 项能力分别是什么、怎么自己复跑一遍,在 [`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md)。
|
|
103
103
|
本节 7 行**全部是 `🔍`** —— 没有一行有自动化守着。
|
|
104
104
|
|
|
105
105
|
| 入口 | UA 自述的环境 | 实测 | 缺什么 |
|
package/ai/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
3
|
"generatedBy": "05-audit/gen-ai-contract.py",
|
|
4
|
-
"libraryVersion": "0.25.
|
|
4
|
+
"libraryVersion": "0.25.6",
|
|
5
5
|
"$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。printPolicy = 这个组件在纸上会发生什么(空数组 = 原样保留);pagination = 内容是否被分页裁过(server 不许与 expand 同时出现,否则展开的只是当前一页)。",
|
|
6
6
|
"maturityCriteria": {
|
|
7
7
|
"alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@media (prefers-reduced-motion:reduce) {*, *::before, *::after {animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}:focus-visible {outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm);}a, button, label, summary, input[type="checkbox"], input[type="radio"], input[type="button"], input[type="submit"], [role="button"], [role="switch"], [role="tab"], [role="option"], .choice, .switch, .btn, .accordion__trigger, .tabs__tab, .card, .list__row {-webkit-tap-highlight-color:transparent;}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root {--raw-paper:#f6f7f8;--raw-surface:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-surface-raised:#ffffff;--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-hover:#8f1e17;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-success:#2f6b3a;--raw-warning:#8a5a00;--raw-info:#1d5f8a;--raw-danger-bg:#fdeeec;--raw-success-bg:#eaf2e6;--raw-warning-bg:#fbf1dd;--raw-info-bg:#e8f1f8;--raw-dir-up:#c0392b;--raw-dir-down:#2f6b3a;--raw-accent:#1b4d8f;--raw-switch-on:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-tooltip-bg:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-tooltip-border:#15181c;--paper:var(--raw-paper);--surface:var(--raw-surface);--surface-sunken:var(--raw-surface-sunken);--surface-raised:var(--raw-surface-raised);--scrollbar-size:10px;--scrollbar-size-thin:6px;--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--scrollbar-radius:5px;--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--skeleton-h:1em;--skeleton-size:40px;--skeleton-duration:1.4s;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--progress-h:6px;--progress-dur:240ms;--r-full:999px;--ring-size:64px;--ring-thickness:8px;--progress:0%;--progress-num:0;--z-popover:60;--popover-min-w:200px;--popover-max-w:320px;--popover-p:var(--sp-3);--popover-gap:8px;--popover-dur:140ms;--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-hover:var(--raw-danger-hover);--scrim:var(--raw-scrim);--success:var(--raw-success);--warning:var(--raw-warning);--info:var(--raw-info);--danger-bg:var(--raw-danger-bg);--success-bg:var(--raw-success-bg);--warning-bg:var(--raw-warning-bg);--info-bg:var(--raw-info-bg);--dir-up:var(--raw-dir-up);--dir-down:var(--raw-dir-down);--accent:var(--raw-accent);--switch-on:var(--raw-switch-on);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-fg:var(--raw-tooltip-fg);--tooltip-border:var(--raw-tooltip-border);--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:clamp(1rem, 0.95rem + 0.3vw, 1.25rem);--sp-5:clamp(1.25rem, 1.15rem + 0.7vw, 1.75rem);--sp-6:clamp(1.5rem, 1.3rem + 1.2vw, 2.25rem);--sp-7:clamp(2rem, 1.7rem + 1.8vw, 3.5rem);--sp-8:clamp(2.5rem, 2.1rem + 2.4vw, 4.5rem);--bp-sm:30rem;--bp-md:48rem;--bp-lg:64rem;--bp-xl:80rem;--fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:clamp(1rem, 0.95rem + 0.3vw, 1.125rem);--fs-md:clamp(1.125rem, 1.05rem + 0.5vw, 1.3125rem);--fs-lg:clamp(1.3125rem, 1.2rem + 0.7vw, 1.5rem);--fs-xl:clamp(1.5rem, 1.3rem + 1.2vw, 1.875rem);--fs-2xl:clamp(1.75rem, 1.45rem + 1.8vw, 2.5rem);--lh-tight:1.25;--lh-base:1.7;--lh-loose:1.85;--measure-page:52rem;--tracking-title:-0.02em;--r-sm:6px;--r-xs:3px;--r-md:10px;--r-lg:14px;--r-pill:999px;--shadow-1:0 1px 2px rgba(31, 29, 25, 0.05), 0 1px 3px rgba(31, 29, 25, 0.06);--shadow-2:0 2px 4px rgba(31, 29, 25, 0.05), 0 6px 16px rgba(31, 29, 25, 0.08);--dur-fast:120ms;--dur-base:180ms;--dur-slow:280ms;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out:cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer:cubic-bezier(0.32, 0.72, 0, 1);--lift-sm:1px;--lift-md:2px;--lift-lg:8px;--z-base:0;--z-sticky:10;--z-header:20;--z-raised:5;--z-overlay:30;--z-modal:40;--z-toast:50;--sz-touch:44px;--sz-touch-sm:32px;}:root {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}:root:lang(en), :root:lang(en-US), :root:lang(en-GB), :root:lang(fr), :root:lang(de), :root:lang(es), :root:lang(pt), :root:lang(ru), :root:lang(el), :root:lang(tr), :root:lang(vi) {--measure-prose:68ch;--measure-title:34ch;--measure-lede:62ch;--measure-pull:58ch;--measure-aside:56ch;}:root:lang(ar) {--measure-prose:60ch;--measure-title:30ch;--measure-lede:56ch;--measure-pull:52ch;--measure-aside:50ch;}:root:lang(hi), :root:lang(th), :root:lang(km), :root:lang(my) {--measure-prose:56ch;--measure-title:28ch;--measure-lede:52ch;--measure-pull:48ch;--measure-aside:46ch;}:root:lang(zh), :root:lang(zh-CN), :root:lang(zh-TW), :root:lang(ja), :root:lang(ko) {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}@media screen and (prefers-color-scheme:dark) {:root:not([data-theme="light"]) {--raw-paper:#14171A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-border-control:#8A9099;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-danger:#E07A6E;--raw-danger-hover:#F09A90;--raw-danger-bg:#341D1A;--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-dir-up:#E07A6E;--raw-dir-down:#6FAA7A;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-accent:#7AA9DE;--raw-switch-on:#3E6EA8;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-tooltip-bg:#232830;--raw-tooltip-fg:#E4E7EA;--raw-tooltip-border:#2A3038;--raw-scrim:rgba(0, 0, 0, 0.62);}}[data-theme="light"] {--raw-accent:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-bg:#fdeeec;--raw-danger-hover:#8f1e17;--raw-dir-down:#2f6b3a;--raw-dir-up:#c0392b;--raw-info:#1d5f8a;--raw-info-bg:#e8f1f8;--raw-paper:#f6f7f8;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-success:#2f6b3a;--raw-success-bg:#eaf2e6;--raw-surface:#ffffff;--raw-surface-raised:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-switch-on:#1b4d8f;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-tooltip-bg:#15181c;--raw-tooltip-border:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-warning:#8a5a00;--raw-warning-bg:#fbf1dd;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}@media screen {:root[data-theme="dark"], [data-theme="dark"] {--raw-accent:#7AA9DE;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-border-control:#8A9099;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-danger:#E07A6E;--raw-danger-bg:#341D1A;--raw-danger-hover:#F09A90;--raw-dir-down:#6FAA7A;--raw-dir-up:#E07A6E;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-paper:#14171A;--raw-scrim:rgba(0, 0, 0, 0.62);--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-switch-on:#3E6EA8;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-tooltip-bg:#232830;--raw-tooltip-border:#2A3038;--raw-tooltip-fg:#E4E7EA;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}}:root {--sans:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Geeza Pro', 'Segoe UI Historic', sans-serif;--mono:ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Consolas, 'Sarasa Mono SC', 'Microsoft YaHei Mono', monospace;--serif:'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Songti SC', 'SimSun', 'Noto Serif CJK SC', 'Source Han Serif SC', serif;--tracking-normal:0;}:root:lang(ar), :root:lang(fa), :root:lang(he), :root:lang(ur) {--tracking-normal:0.01em;}
|
|
@@ -188,11 +188,13 @@ return String(s).replace(/[&<>"]/g, function (c) {
|
|
|
188
188
|
var q = input.value.trim();
|
|
189
189
|
shown = filter(q).slice(0, 50);
|
|
190
190
|
if (!shown.length) {
|
|
191
|
+
/* @html-sink escaped — 插值只有 q,且过了 esc() */
|
|
191
192
|
list.innerHTML = '<li class="combo__empty" role="presentation">' +
|
|
192
193
|
(q ? '没有匹配「' + esc(q) + '」的选项' : '暂无可选') + '</li>';
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
var ql = q.toLowerCase();
|
|
197
|
+
/* @html-sink escaped — label/value 全部过 esc();list.id / i / picked 是内部量 */
|
|
196
198
|
list.innerHTML = shown.map(function (o, i) {
|
|
197
199
|
var picked = tags.indexOf(o.value) >= 0;
|
|
198
200
|
var label = esc(o.label);
|
|
@@ -217,6 +219,7 @@ return String(s).replace(/[&<>"]/g, function (c) {
|
|
|
217
219
|
/* ---------- 标签 ---------- */
|
|
218
220
|
function paintTags() {
|
|
219
221
|
if (!tagBox) return;
|
|
222
|
+
/* @html-sink escaped — o.label 与 v 都过 esc() */
|
|
220
223
|
tagBox.innerHTML = tags.map(function (v) {
|
|
221
224
|
var o = ALL.filter(function (x) { return x.value === v; })[0] || { label: v };
|
|
222
225
|
/* ⭐ 删除按钮有 aria-label(否则读屏只念"×")*/
|