agentcss 0.25.4 → 0.25.5
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 +83 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +18 -1
- package/README.zh-CN.md +39 -14
- package/SECURITY.md +28 -9
- package/START-HERE.md +7 -7
- 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/index.html +1 -1
- package/llms-full.txt +1 -1
- package/package.json +3 -1
|
@@ -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,89 @@
|
|
|
16
16
|
|
|
17
17
|
(无)
|
|
18
18
|
|
|
19
|
+
## [0.25.5] - 2026-11-04
|
|
20
|
+
|
|
21
|
+
### 新增:`tokens.core.css`(只有变量)与 `base.reset.css`(只有底座)
|
|
22
|
+
|
|
23
|
+
`tokens.css` 一直同时干两件事:定义变量,以及**改整个宿主文档**的三条规则
|
|
24
|
+
(全局 `:focus-visible`、`a/button/label…` 的 `-webkit-tap-highlight-color`、
|
|
25
|
+
`prefers-reduced-motion` 里对 `*` 的 `!important`)。README 的最小示例却把这套组合
|
|
26
|
+
描述成"零副作用"——嵌进已有站点的人会发现宿主的焦点环与点按反馈被换掉了,
|
|
27
|
+
而看不出是哪一行。
|
|
28
|
+
|
|
29
|
+
冻结期不能改 `tokens.css` 的语义,所以只做加法:
|
|
30
|
+
|
|
31
|
+
| 新入口 | 含什么 | 改宿主吗 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `agentcss/tokens.core.css` | 只有变量(含暗色、按语言的字体与字距) | 不会 |
|
|
34
|
+
| `agentcss/base.reset.css` | 只有那三条 | 会 |
|
|
35
|
+
| `agentcss/tokens.css` | 旧路,一个字节没动 | 会(同上) |
|
|
36
|
+
|
|
37
|
+
两个新文件是**生成物**(`05-audit/split-tokens.py` 从 `tokens.css` 切出来),
|
|
38
|
+
`token-split` 门禁逐字节比对 —— 同一事实不留第二份手写来源(I-7)。
|
|
39
|
+
|
|
40
|
+
**计算样式实测**(light / dark 各一次,同一页面只换引入文件):
|
|
41
|
+
|
|
42
|
+
| | `--paper` | 焦点环 | 点按高亮 |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| core | `#f6f7f8` / `#14171A` | `auto 1px`(浏览器默认) | 保留 `0.18` |
|
|
45
|
+
| tokens.css | `#f6f7f8` / `#14171A` | `solid 2px`(库的) | 被关掉 `0` |
|
|
46
|
+
|
|
47
|
+
判据 5 条 + 反向控制 4 条。核心一条:**每条顶层规则必须归入 core / reset / library**,
|
|
48
|
+
归不了类就红 —— 将来有人往 `tokens.css` 加第四条全局规则,会被逼着做一次显式决定。
|
|
49
|
+
|
|
50
|
+
过程中自己踩的两个坑,都写进了判据:
|
|
51
|
+
- 按选择器前缀归类 ⇒ 暗色令牌在 `@media screen and (prefers-color-scheme: dark)` 里,
|
|
52
|
+
被判成"库自己的东西",core 里没有暗色;**计算样式实测 dark 下仍返回 `#f6f7f8`**。
|
|
53
|
+
改成递归判"块里是不是只有自定义属性"。
|
|
54
|
+
- `_inner()` 在原文里找第一个 `{`,而块首注释里就有 `{`(举例 `.wrap { max-width }`)
|
|
55
|
+
⇒ 切到注释中间,后面全错。索引必须从遮蔽层取。
|
|
56
|
+
|
|
57
|
+
### 新增:每个 HTML 注入点都要说得清装的是什么(`xss-sink-gate`)
|
|
58
|
+
|
|
59
|
+
全库 21 处 `.innerHTML` / `.insertAdjacentHTML`,此前没有任何机制阻止第 22 处悄悄进来,
|
|
60
|
+
`SECURITY.md` 只写了 `combobox` 一处、并称"没有门禁守着"。现在分四类
|
|
61
|
+
(清空 12 / 已转义 3 / 内部量 4 / 有意入口 2),非"赋空串"的必须在紧邻上一行写
|
|
62
|
+
`/* @html-sink <kind> — <理由> */`。
|
|
63
|
+
|
|
64
|
+
两处**真缺陷**已修:
|
|
65
|
+
- `model-viewer` 把 `src` 的文件名拼进 `innerHTML`。文件名可以是
|
|
66
|
+
`<img src=x onerror=…>`。改成 `textContent` 组装。载荷实测:注入串只以
|
|
67
|
+
`<img` 出现,`injectedImg=false`、`flag=false`。
|
|
68
|
+
- `date-range` 把 `<input>` 的 `value` 拼进 `innerHTML`,而 `type=date` 在
|
|
69
|
+
Chromium 97 旧内核会回退成文本输入 ⇒ `value` 可为任意串。改成节点组装(摘要文本不变,
|
|
70
|
+
组件契约门禁仍断言「区间 2026-01-01 → 2026-01-31(31 天)」)。
|
|
71
|
+
|
|
72
|
+
`raw-html` 两处(`popover` 的 `opts.html`、`drawer` 的 `opt.content`)登记在
|
|
73
|
+
`SECURITY.md` 的「HTML 入口」,与代码**双向**核对(陈旧登记也红)。
|
|
74
|
+
|
|
75
|
+
**这道门禁证明不了什么**:它不证明 `esc()` 的实现对,也不覆盖全部数据流。
|
|
76
|
+
它证明的是"新增未声明的注入点必须显式改标记或改文档"。
|
|
77
|
+
|
|
78
|
+
### 修正:文档里三处正在说谎的地方
|
|
79
|
+
|
|
80
|
+
- `CONVENTIONS.md` 写「`!important` 现有 10 处(无障碍 9 + `[hidden]` 1)」,
|
|
81
|
+
门禁实测 **13 处**:多出来的 2 处是 `@media print` —— 文档连**类别**都没列。
|
|
82
|
+
按文档去审计的人会把这两处正当的写成"说不清为什么要赢"。改成类别表 + 「要数字跑门禁」。
|
|
83
|
+
- `README` 说「`components.json` 里标 `beta` 的那几个组件」,实际 **40/40 stable**。
|
|
84
|
+
改成讲清四条机械判据,并写明 `stable` 量的是机制完备度、不是"久经生产"。
|
|
85
|
+
- `README` 安装段还留着「发布成功之后这一行才会换上」的历史段落(上一句已写已发布);
|
|
86
|
+
源码复制那段编号从 ② 改 ③。
|
|
87
|
+
|
|
88
|
+
### 体积账本:去掉重复计数,不放宽上限
|
|
89
|
+
|
|
90
|
+
`tokens.core.css` / `base.reset.css` 是 `tokens.css` 的切片,消费者只下载其中一个。
|
|
91
|
+
三个文件一起计入会把同一批声明算三遍 ⇒ `perf-gate` 排除这两个**已知切片**并在输出里
|
|
92
|
+
打印说明;`ceiling` / `granted` 一个都没动。
|
|
93
|
+
|
|
94
|
+
### 验证
|
|
95
|
+
|
|
96
|
+
- 快速层 **120 道 / 0 FAIL**;打包冒烟(含消费者侧文档路径解析)通过。
|
|
97
|
+
- 视觉基线重录,但 **109 张 PNG 逐字节未变**,只有 `_stamp.json` 更新。
|
|
98
|
+
- 反向控制:`xss-sink` 12 条、`token-split` 4 条,每条都证明"该红时真的红"。
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
19
102
|
## [0.25.4] - 2026-11-03
|
|
20
103
|
|
|
21
104
|
**修订版:把「类型里看不见的那 8 个全局」补上,并让这件事以后有人管。
|
package/CONTRIBUTING.md
CHANGED
|
@@ -29,7 +29,7 @@ npm run gate # 全量:含浏览器类
|
|
|
29
29
|
npm run gate:fast # 只跑静态层,不用装浏览器
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
全量是 **
|
|
32
|
+
全量是 **239 道门禁**(默认 `min(核数, 6)` 路并发,实测 583 秒 / 退出码 0;
|
|
33
33
|
单道耗时累加 1658 秒,并发效率约 2.8×。组件层要起浏览器,还要逐张比 **109 张**视觉截图 ——
|
|
34
34
|
47 浅色 + 47 暗色 + 15 RTL,按平台分集,只比本平台那套)。
|
|
35
35
|
只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
|
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`).
|
|
@@ -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,10 +276,11 @@ 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
285
|
**一个冻结的上限。** 整套库受一份账本约束([`05-audit/budget.json`](05-audit/budget.json)):
|
|
265
286
|
硬天花板 + 一份「常规新增组件不用改任何数字」的储备 + 一个只在体积下降时收紧的棘轮。
|
|
@@ -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/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.5",
|
|
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(否则读屏只念"×")*/
|
|
@@ -146,7 +146,14 @@ return true;
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
function paintSummary(s) {
|
|
149
|
-
|
|
149
|
+
|
|
150
|
+
if (!summary) return;
|
|
151
|
+
summary.textContent = '';
|
|
152
|
+
if (!s) return;
|
|
153
|
+
summary.appendChild(document.createTextNode('区间 '));
|
|
154
|
+
var b = document.createElement('b');
|
|
155
|
+
b.textContent = s;
|
|
156
|
+
summary.appendChild(b);
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
function emit() {
|
|
@@ -186,6 +193,7 @@ presets = presets.filter(function (x) { return opt.presets.indexOf(x.key) >= 0;
|
|
|
186
193
|
}
|
|
187
194
|
var box = root.querySelector('[data-dr-presets]');
|
|
188
195
|
if (!box) return;
|
|
196
|
+
|
|
189
197
|
box.innerHTML = presets.map(function (p) {
|
|
190
198
|
return '<button class="drange__preset" type="button" aria-pressed="false" ' +
|
|
191
199
|
'data-dr-preset="'+ p.key + '">' + p.label + '</button>';
|
|
@@ -148,6 +148,7 @@ box.setAttribute('inert', '');
|
|
|
148
148
|
|
|
149
149
|
box.setAttribute('role', 'dialog');
|
|
150
150
|
box.setAttribute('aria-label', opts.label || '');
|
|
151
|
+
|
|
151
152
|
box.innerHTML =
|
|
152
153
|
'<span class="popover__arrow" aria-hidden="true"></span>'+
|
|
153
154
|
'<div class="popover__body">'+ (opts.html || '') + '</div>';
|
|
@@ -75,6 +75,7 @@ btn.type = 'button';
|
|
|
75
75
|
btn.className = 'shell__menu';
|
|
76
76
|
btn.setAttribute('aria-label', o.label || ('打开' + title));
|
|
77
77
|
btn.setAttribute('aria-haspopup', 'dialog');
|
|
78
|
+
|
|
78
79
|
btn.innerHTML = ICON;
|
|
79
80
|
btn.addEventListener('click', function () { self.open(); });
|
|
80
81
|
topbar.insertBefore(btn, topbar.firstChild);
|
|
@@ -68,8 +68,14 @@ this.btn.addEventListener('click', function () { self.load(); });
|
|
|
68
68
|
this.ui.appendChild(this.btn);
|
|
69
69
|
|
|
70
70
|
this.hint = el('p', 'mv__hint');
|
|
71
|
-
|
|
72
|
-
'
|
|
71
|
+
|
|
72
|
+
var fname = el('code', null, this.src.split('/').pop() || '—');
|
|
73
|
+
var strong = el('b', null, '点击后才下载');
|
|
74
|
+
this.hint.appendChild(document.createTextNode('模型 '));
|
|
75
|
+
this.hint.appendChild(fname);
|
|
76
|
+
this.hint.appendChild(document.createTextNode(' 会'));
|
|
77
|
+
this.hint.appendChild(strong);
|
|
78
|
+
this.hint.appendChild(document.createTextNode(',首屏不占流量。'));
|
|
73
79
|
this.ui.appendChild(this.hint);
|
|
74
80
|
|
|
75
81
|
if (!hasWebGL()) {
|
|
@@ -260,6 +266,7 @@ self.raf = requestAnimationFrame(loop);
|
|
|
260
266
|
self.ui.innerHTML = '';
|
|
261
267
|
var cap = el('p', 'mv__cap');
|
|
262
268
|
var dt = Math.round(((window.performance && performance.now) ? performance.now() : Date.now()) - t0);
|
|
269
|
+
|
|
263
270
|
cap.innerHTML = '模型 <b>' + fmtKB(bytes || self.lastBytes || 0) + '</b> · 引擎就绪耗时 <b>' + dt + 'ms</b>' +
|
|
264
271
|
(controls ? ' · 拖动可旋转,滚轮/双指可缩放' : ' · 未加载控制器,无法拖动');
|
|
265
272
|
self.node.appendChild(cap);
|