agentcss 0.25.6 → 0.27.0
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/README.md +1 -1
- package/01-tokens/behavior/labels.js +44 -0
- package/01-tokens/responsive-demo.html +1 -1
- package/01-tokens/tokens.core.css +1 -1
- package/01-tokens/tokens.css +2 -2
- package/02-primitives/combobox/combobox.js +26 -3
- package/02-primitives/date-range/date-range.js +28 -6
- package/02-primitives/popover/popover.js +22 -2
- package/02-primitives/select/select.js +23 -2
- package/02-primitives/timeline/demo.html +1 -1
- package/02-primitives/upload/upload.js +50 -17
- package/03-patterns/content/content.css +13 -1
- package/03-patterns/content/demo.html +1 -1
- package/03-patterns/drawer/demo.html +1 -1
- package/03-patterns/drawer/drawer.js +44 -6
- package/03-patterns/dropdown/dropdown.js +21 -1
- package/03-patterns/overlay/overlay.css +1 -1
- package/03-patterns/overlay/overlay.js +39 -2
- package/03-patterns/pagination/pagination.js +37 -10
- package/03-patterns/states/states.css +1 -1
- package/03-patterns/tooltip/tooltip.js +17 -2
- package/03-patterns/tree/tree.js +21 -1
- package/04-recipes/analysis-report/demo.html +1 -1
- package/04-recipes/app-shell/README.md +1 -1
- package/04-recipes/app-shell/demo.html +1 -1
- package/04-recipes/data-showcase/demo.html +2 -2
- package/04-recipes/longform/content.html +993 -78
- package/04-recipes/longform/longform.html +3 -3
- package/04-recipes/report/demo.html +3 -3
- package/04-recipes/table/README.md +23 -1
- package/04-recipes/table/demo.html +7 -7
- package/09-assets/bar/bar.css +1 -1
- package/09-assets/bar/bar.js +28 -4
- package/09-assets/model-viewer/model-viewer.js +45 -13
- package/09-assets/scientific-plot/demo.html +6 -6
- package/AGENTS.md +3 -1
- package/API.md +32 -0
- package/CHANGELOG.md +174 -5
- package/CONTRIBUTING.md +44 -3
- package/README.md +19 -0
- package/README.zh-CN.md +18 -0
- package/SECURITY.md +16 -4
- package/START-HERE.md +25 -18
- package/ai/START-HERE.md +40 -6
- package/ai/check-html.js +658 -0
- package/ai/cli.js +557 -67
- package/ai/components.json +9 -4
- package/ai/pdf.js +52 -10
- package/ai/recipes.json +86 -0
- package/ai/scaffold.js +195 -0
- package/ai/shot.js +345 -0
- package/ai/validate.js +65 -6
- package/dist/01-tokens/behavior/labels.js +11 -0
- package/dist/02-primitives/combobox/combobox.js +25 -3
- package/dist/02-primitives/date-range/date-range.js +26 -5
- package/dist/02-primitives/popover/popover.js +14 -1
- package/dist/02-primitives/select/select.js +22 -2
- package/dist/02-primitives/upload/upload.js +47 -17
- package/dist/03-patterns/content/content.css +1 -1
- package/dist/03-patterns/drawer/drawer.js +32 -4
- package/dist/03-patterns/dropdown/dropdown.js +20 -1
- package/dist/03-patterns/overlay/overlay.js +25 -1
- package/dist/03-patterns/pagination/pagination.js +34 -10
- package/dist/03-patterns/tooltip/tooltip.js +14 -2
- package/dist/03-patterns/tree/tree.js +20 -1
- package/dist/09-assets/bar/bar.js +23 -4
- package/dist/09-assets/model-viewer/model-viewer.js +38 -12
- package/dist/COSTS.md +20 -20
- package/docs/ACCESSIBILITY.md +3 -3
- package/docs/ARCHITECTURE.md +116 -0
- package/docs/L10N.md +92 -0
- package/docs/LAYER-BASELINE.md +2 -2
- package/docs/PRINT.md +3 -3
- package/docs/RELEASE-PROCESS.md +59 -3
- package/docs/STABILITY.md +1 -1
- package/docs/VERIFY-RELEASE.md +24 -2
- package/index.html +3 -3
- package/llms-full.txt +18 -4
- package/llms.txt +26 -5
- package/package.json +4 -2
- package/types/index.d.ts +218 -13
package/01-tokens/README.md
CHANGED
|
@@ -257,7 +257,7 @@ node ai/cli.js tree [--set=light|dark] [--cat=color] # 第二种
|
|
|
257
257
|
(`contrast` / `gen-ai-contract` / `gen-tokens-tree` / `token-audience` /
|
|
258
258
|
`theme-sync`)。真实事故:一条 `@layer` 让六个工具同时读错,且都没有报错。
|
|
259
259
|
3. 选择器是 `:root[data-market="intl"]` (0,2,0),**不是** `:where(...)` (0) ——
|
|
260
|
-
`:where()` 抢不过 `:root` 的 (0,1,0)
|
|
260
|
+
`:where()` 抢不过 `:root` 的 (0,1,0),写了等于没写。
|
|
261
261
|
|
|
262
262
|
> 精简档(`tierA-tokens.css`)**不含**这段交换:它是一份 `<style>` 内的
|
|
263
263
|
> 令牌快照,没有配套文件可引。要国际口径就自己补一条规则 ——
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* labels.js — 「文案覆盖合并」工具核
|
|
3
|
+
*
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* 🔴 为什么要有这个核
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
* 0.26.0 给组件加 `labels`(读屏 / 可见文案覆盖)时,最初的写法是
|
|
8
|
+
* 在 9 个组件里各贴一份合并函数:
|
|
9
|
+
*
|
|
10
|
+
* for (k in defaults) { out[k] = src[k] == null ? defaults[k] : src[k]; }
|
|
11
|
+
*
|
|
12
|
+
* 九份一模一样的循环,只差 `defaults` 的字面量。这与 I-14 是同一条病:
|
|
13
|
+
* **同一份逻辑写 N 份**。后果不是"难看",是**修正只到一处**:
|
|
14
|
+
* · 以后要支持"传 null 表示删掉这条文案"→ 改 1 处、漏 8 处;
|
|
15
|
+
* · 要支持嵌套命名空间 → 同上;
|
|
16
|
+
* · 某处写成 `src[k] || defaults[k]`,空字符串就再也覆盖不掉 ——
|
|
17
|
+
* 而这种偏差在 9 份副本里肉眼几乎看不出来。
|
|
18
|
+
*
|
|
19
|
+
* ⇒ 真值源一份(本文件),生成进使用点,`gen-behavior --check` 证明跟随。
|
|
20
|
+
* 使用点仍然**各自保留自己的 `defaults` 字面量** —— 那是组件的事实,
|
|
21
|
+
* 不是可以共享的代码。
|
|
22
|
+
*
|
|
23
|
+
* ==========================================================================
|
|
24
|
+
* 语义(三句话,必须与实现逐字对应)
|
|
25
|
+
* --------------------------------------------------------------------------
|
|
26
|
+
* ① `undefined` / `null` ⇒ 用默认值("没传"和"显式取消"等价);
|
|
27
|
+
* ② 其他任何值(含 `''`、`0`、`false`)⇒ **原样采用** ——
|
|
28
|
+
* 用 `||` 判断会把空字符串吞掉,那是错误的;
|
|
29
|
+
* ③ 只认 `defaults` 里有的键 —— 拼错的键不会悄悄生效(避免"传了却没反应")。
|
|
30
|
+
* ==========================================================================
|
|
31
|
+
*/
|
|
32
|
+
/* ==== BEHAVIOR CORE BEGIN: labels ==== */
|
|
33
|
+
var flLabels = function (defaults, src) {
|
|
34
|
+
src = src || {};
|
|
35
|
+
var out = {};
|
|
36
|
+
var k;
|
|
37
|
+
for (k in defaults) {
|
|
38
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
39
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
44
|
+
/* ==== BEHAVIOR CORE END: labels ==== */
|
|
@@ -76,7 +76,7 @@ p { margin: 0 0 var(--sp-4); }
|
|
|
76
76
|
</div>
|
|
77
77
|
<p style="margin-top:var(--sp-4)">
|
|
78
78
|
判据很简单:<strong>这个值变了会不会导致"控件大小跟着变"?会 → 不要流体。</strong>
|
|
79
|
-
|
|
79
|
+
<code>--fs-xs</code> / <code>--fs-sm</code>(UI 文本)和 <code>--sp-1</code>~<code>--sp-3</code>(小间距)都属这一类。
|
|
80
80
|
</p>
|
|
81
81
|
|
|
82
82
|
<h2>三、该用断点的:布局结构</h2>
|
|
@@ -884,7 +884,7 @@
|
|
|
884
884
|
—— 遍历所有声明、后者覆盖前者、**不看选择器**。
|
|
885
885
|
把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
|
|
886
886
|
于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
|
|
887
|
-
|
|
887
|
+
比漏判更难查(实测)。
|
|
888
888
|
⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
|
|
889
889
|
========================================================================= */
|
|
890
890
|
|
package/01-tokens/tokens.css
CHANGED
|
@@ -926,7 +926,7 @@
|
|
|
926
926
|
—— 遍历所有声明、后者覆盖前者、**不看选择器**。
|
|
927
927
|
把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
|
|
928
928
|
于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
|
|
929
|
-
|
|
929
|
+
比漏判更难查(实测)。
|
|
930
930
|
⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
|
|
931
931
|
========================================================================= */
|
|
932
932
|
|
|
@@ -1117,7 +1117,7 @@ input[type="submit"],
|
|
|
1117
1117
|
/* ⚠️ 模态的遮罩规则**不在这里**,在 03-patterns/overlay/overlay.css。
|
|
1118
1118
|
原因不是洁癖:`.dialog-backdrop { position: static }` 写在这里会被
|
|
1119
1119
|
overlay.css 里同特异性的 `position: fixed` 盖掉 —— 规则写了、不生效、
|
|
1120
|
-
|
|
1120
|
+
不报错(同一个形状,2026-11-02 由 print-policy-gate 的 L3 抓到)。 */
|
|
1121
1121
|
|
|
1122
1122
|
/* 不跨页切断。代价:高于半页的卡片会被整体推到下一页 ⇒ 页数可能变多。 */
|
|
1123
1123
|
tr, img, figure, svg,
|
|
@@ -145,6 +145,29 @@
|
|
|
145
145
|
var uid = 0;
|
|
146
146
|
|
|
147
147
|
function create(root, opt) {
|
|
148
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
149
|
+
var DEFAULT_LABELS = {
|
|
150
|
+
suggest: '建议',
|
|
151
|
+
input: '输入',
|
|
152
|
+
noMatch: function (q) { return '没有匹配「' + q + '」的选项'; },
|
|
153
|
+
empty: '暂无可选'
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
157
|
+
var flLabels = function (defaults, src) {
|
|
158
|
+
src = src || {};
|
|
159
|
+
var out = {};
|
|
160
|
+
var k;
|
|
161
|
+
for (k in defaults) {
|
|
162
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
163
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
};
|
|
168
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
169
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
170
|
+
|
|
148
171
|
opt = opt || {};
|
|
149
172
|
var input = root.querySelector('[data-combo-input]');
|
|
150
173
|
var list = root.querySelector('[data-combo-list]');
|
|
@@ -182,14 +205,14 @@
|
|
|
182
205
|
var shown = []; /* 当前过滤后的选项 */
|
|
183
206
|
|
|
184
207
|
list.setAttribute('role', 'listbox');
|
|
185
|
-
list.setAttribute('aria-label', opt.label ||
|
|
208
|
+
list.setAttribute('aria-label', opt.label || L.suggest);
|
|
186
209
|
input.setAttribute('role', 'combobox');
|
|
187
210
|
input.setAttribute('aria-autocomplete', 'list');
|
|
188
211
|
input.setAttribute('aria-expanded', 'false');
|
|
189
212
|
input.setAttribute('aria-controls', list.id);
|
|
190
213
|
if (!input.id) input.id = 'combo-input-' + seq;
|
|
191
214
|
if (!input.getAttribute('aria-label') && !root.querySelector('label[for="' + input.id + '"]')) {
|
|
192
|
-
input.setAttribute('aria-label', opt.label ||
|
|
215
|
+
input.setAttribute('aria-label', opt.label || L.input);
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
/* ---------- 过滤(含高亮命中片段)---------- */
|
|
@@ -226,7 +249,7 @@
|
|
|
226
249
|
if (!shown.length) {
|
|
227
250
|
/* @html-sink escaped — 插值只有 q,且过了 esc() */
|
|
228
251
|
list.innerHTML = '<li class="combo__empty" role="presentation">' +
|
|
229
|
-
(q ?
|
|
252
|
+
(q ? L.noMatch(esc(q)) : L.empty) + '</li>';
|
|
230
253
|
return;
|
|
231
254
|
}
|
|
232
255
|
var ql = q.toLowerCase();
|
|
@@ -119,6 +119,28 @@
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function create(root, opt) {
|
|
122
|
+
/* 校验文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
123
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
124
|
+
var flLabels = function (defaults, src) {
|
|
125
|
+
src = src || {};
|
|
126
|
+
var out = {};
|
|
127
|
+
var k;
|
|
128
|
+
for (k in defaults) {
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
130
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
136
|
+
var DEFAULT_LABELS = {
|
|
137
|
+
requiredEnd: '请填写结束日期。',
|
|
138
|
+
requiredStart: '请填写开始日期。',
|
|
139
|
+
order: function (f, t) { return '开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。'; },
|
|
140
|
+
min: function (min) { return '开始日期不能早于 ' + min + '。'; },
|
|
141
|
+
max: function (max) { return '结束日期不能晚于 ' + max + '。'; }
|
|
142
|
+
};
|
|
143
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
122
144
|
opt = opt || {};
|
|
123
145
|
var from = root.querySelector('[data-dr-from]');
|
|
124
146
|
var to = root.querySelector('[data-dr-to]');
|
|
@@ -158,21 +180,21 @@
|
|
|
158
180
|
function validate() {
|
|
159
181
|
var f = from.value, t = to.value;
|
|
160
182
|
if (!f && !t) { setError(''); paintSummary(''); return true; }
|
|
161
|
-
if (f && !t) { setError(
|
|
162
|
-
if (!f && t) { setError(
|
|
183
|
+
if (f && !t) { setError(L.requiredEnd); return false; }
|
|
184
|
+
if (!f && t) { setError(L.requiredStart); return false; }
|
|
163
185
|
if (f > t) {
|
|
164
|
-
setError(
|
|
186
|
+
setError(L.order(f, t));
|
|
165
187
|
return false;
|
|
166
188
|
}
|
|
167
|
-
if (opt.min && f < opt.min) { setError(
|
|
168
|
-
if (opt.max && t > opt.max) { setError(
|
|
189
|
+
if (opt.min && f < opt.min) { setError(L.min(opt.min)); return false; }
|
|
190
|
+
if (opt.max && t > opt.max) { setError(L.max(opt.max)); return false; }
|
|
169
191
|
setError('');
|
|
170
192
|
paintSummary(f + ' → ' + t + '(' + days(f, t) + ' 天)');
|
|
171
193
|
return true;
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
function paintSummary(s) {
|
|
175
|
-
/* 🔴 不拼 innerHTML
|
|
197
|
+
/* 🔴 不拼 innerHTML:`s` 来自两个 <input> 的 value,
|
|
176
198
|
而 `type=date` 在 Chromium 97 旧内核会回退成文本输入 ⇒ value 可为任意串。 */
|
|
177
199
|
if (!summary) return;
|
|
178
200
|
summary.textContent = '';
|
|
@@ -181,11 +181,12 @@
|
|
|
181
181
|
(加了就是模态,语义就错了)。 */
|
|
182
182
|
box.setAttribute('role', 'dialog');
|
|
183
183
|
box.setAttribute('aria-label', opts.label || '');
|
|
184
|
+
var bodyHtml = sanitizeHtml(opts.html || '', opts.sanitize);
|
|
184
185
|
/* @html-sink raw-html — `opts.html` 是有意的 HTML 入口,调用方负责净化;
|
|
185
|
-
登记在 SECURITY.md「HTML 入口(sink
|
|
186
|
+
登记在 SECURITY.md「HTML 入口(sink)」。`sanitize` / `setSanitizer` 可挂钩。 */
|
|
186
187
|
box.innerHTML =
|
|
187
188
|
'<span class="popover__arrow" aria-hidden="true"></span>' +
|
|
188
|
-
'<div class="popover__body">' +
|
|
189
|
+
'<div class="popover__body">' + bodyHtml + '</div>';
|
|
189
190
|
|
|
190
191
|
/* 箭头要指向触发元素的中心,而不是浮层的中心 */
|
|
191
192
|
positionArrow(box, trigger);
|
|
@@ -294,6 +295,24 @@
|
|
|
294
295
|
return api;
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
/* HTML 净化钩子(F6 / 0.26.0)。
|
|
299
|
+
🔴 默认**不改变任何行为**:没传 `sanitize` 也没有 `setSanitizer()` 时,
|
|
300
|
+
`opts.html` 仍然按"调用方已净化"处理(与 0.25.x 一致)。
|
|
301
|
+
⇒ 想接入 DOMPurify 这类库:
|
|
302
|
+
Popover.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
|
|
303
|
+
或逐次传 `opts.sanitize` 覆盖(逐次优先于模块级)。
|
|
304
|
+
⚠️ 钩子只处理**字符串入口**;传 Node 的入口本来就绕过了 HTML 解析。 */
|
|
305
|
+
var SANITIZER = null;
|
|
306
|
+
|
|
307
|
+
function setSanitizer(fn) {
|
|
308
|
+
SANITIZER = (typeof fn === 'function') ? fn : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sanitizeHtml(html, perCall) {
|
|
312
|
+
var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
|
|
313
|
+
return fn ? String(fn(String(html))) : String(html);
|
|
314
|
+
}
|
|
315
|
+
|
|
297
316
|
/* 箭头对准触发元素中心(浮层可能比触发元素宽) */
|
|
298
317
|
function positionArrow(box, trigger) {
|
|
299
318
|
var arrow = box.querySelector('.popover__arrow');
|
|
@@ -321,5 +340,6 @@
|
|
|
321
340
|
get current() { return current; },
|
|
322
341
|
};
|
|
323
342
|
|
|
343
|
+
Popover.setSanitizer = setSanitizer;
|
|
324
344
|
global.Popover = Popover;
|
|
325
345
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -246,6 +246,27 @@
|
|
|
246
246
|
var uid = 0;
|
|
247
247
|
|
|
248
248
|
function create(root, opt) {
|
|
249
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
250
|
+
var DEFAULT_LABELS = {
|
|
251
|
+
option: '选项',
|
|
252
|
+
placeholder: '请选择'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
256
|
+
var flLabels = function (defaults, src) {
|
|
257
|
+
src = src || {};
|
|
258
|
+
var out = {};
|
|
259
|
+
var k;
|
|
260
|
+
for (k in defaults) {
|
|
261
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
262
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
};
|
|
267
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
268
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
269
|
+
|
|
249
270
|
opt = opt || {};
|
|
250
271
|
var btn = root.querySelector('[data-select-btn]');
|
|
251
272
|
var list = root.querySelector('[data-select-list]');
|
|
@@ -299,7 +320,7 @@
|
|
|
299
320
|
|
|
300
321
|
list.setAttribute('role', 'listbox');
|
|
301
322
|
if (isMulti) list.setAttribute('aria-multiselectable', 'true');
|
|
302
|
-
list.setAttribute('aria-label', opt.label ||
|
|
323
|
+
list.setAttribute('aria-label', opt.label || L.option);
|
|
303
324
|
btn.setAttribute('aria-haspopup', 'listbox');
|
|
304
325
|
btn.setAttribute('aria-expanded', 'false');
|
|
305
326
|
btn.setAttribute('aria-controls', list.id);
|
|
@@ -344,7 +365,7 @@
|
|
|
344
365
|
for (var i = 0; i < options.length; i++) {
|
|
345
366
|
if (hasValue(valueOfOpt(options[i]))) texts.push(optText(options[i]));
|
|
346
367
|
}
|
|
347
|
-
return texts.length ? texts.join('、') : (opt.placeholder ||
|
|
368
|
+
return texts.length ? texts.join('、') : (opt.placeholder || L.placeholder);
|
|
348
369
|
}
|
|
349
370
|
/* ⚠️ 空列表也会被调用(初始化 / 清空后重绘):必须能接住 undefined,
|
|
350
371
|
老实现直接 o.querySelector 会抛 "Cannot read properties of undefined"。 */
|
|
@@ -80,8 +80,42 @@
|
|
|
80
80
|
remove: 'M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6'
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/* 读屏 / 状态文案默认值(F2 / 0.26.0)。
|
|
84
|
+
值可以是字符串或函数;不传 `labels` 时输出与之前逐字相同。 */
|
|
85
|
+
var DEFAULT_LABELS = {
|
|
86
|
+
pending: '待上传',
|
|
87
|
+
done: '已完成',
|
|
88
|
+
error: '失败',
|
|
89
|
+
remove: function (name) { return '移除 ' + name; },
|
|
90
|
+
uploading: function (pct) { return '上传中 ' + Math.round(pct) + '%'; },
|
|
91
|
+
countMax: function (n) { return '最多 ' + n + ' 个文件'; },
|
|
92
|
+
sizeMax: function (limit) { return '超过 ' + limit; },
|
|
93
|
+
type: '类型不支持',
|
|
94
|
+
folder: '请拖入文件,不支持文件夹',
|
|
95
|
+
none: function (reason) { return reason ? '未添加任何文件:' + reason : '未添加任何文件'; },
|
|
96
|
+
noDrop: '当前浏览器不支持拖放,请点「选择文件」',
|
|
97
|
+
noDropShort: '当前浏览器不支持拖放,请点选择文件',
|
|
98
|
+
summary: function (added, total, rejected) {
|
|
99
|
+
var s = '已添加 ' + added + ' 个文件,共 ' + total + ' 个';
|
|
100
|
+
if (rejected) s += ';' + rejected + ' 个未通过校验';
|
|
101
|
+
return s;
|
|
102
|
+
},
|
|
103
|
+
removed: function (name, rest) { return '已移除 ' + name + ',还剩 ' + rest + ' 个'; }
|
|
104
|
+
};
|
|
84
105
|
|
|
106
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
107
|
+
var flLabels = function (defaults, src) {
|
|
108
|
+
src = src || {};
|
|
109
|
+
var out = {};
|
|
110
|
+
var k;
|
|
111
|
+
for (k in defaults) {
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
113
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
85
119
|
/* ---------------------------------------------------------------- 工具 */
|
|
86
120
|
|
|
87
121
|
/**
|
|
@@ -183,6 +217,7 @@
|
|
|
183
217
|
var opt = opts || {};
|
|
184
218
|
|
|
185
219
|
this.root = root;
|
|
220
|
+
this.labels = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
186
221
|
this.input = root.querySelector('.upload__input');
|
|
187
222
|
this.list = root.querySelector('.upload__list');
|
|
188
223
|
this.status = root.querySelector('.upload__status');
|
|
@@ -299,15 +334,15 @@
|
|
|
299
334
|
for (var i = 0; i < incoming.length; i++) {
|
|
300
335
|
var f = incoming[i];
|
|
301
336
|
if (maxFiles && keep.length >= maxFiles) {
|
|
302
|
-
rejected.push({ file: f, reason: 'count', text:
|
|
337
|
+
rejected.push({ file: f, reason: 'count', text: this.labels.countMax(maxFiles) });
|
|
303
338
|
continue;
|
|
304
339
|
}
|
|
305
340
|
if (this.maxSize && f.size > this.maxSize) {
|
|
306
|
-
rejected.push({ file: f, reason: 'size', text:
|
|
341
|
+
rejected.push({ file: f, reason: 'size', text: this.labels.sizeMax(humanSize(this.maxSize)) });
|
|
307
342
|
continue;
|
|
308
343
|
}
|
|
309
344
|
if (!matchesAccept(f, accept)) {
|
|
310
|
-
rejected.push({ file: f, reason: 'type', text:
|
|
345
|
+
rejected.push({ file: f, reason: 'type', text: this.labels.type });
|
|
311
346
|
continue;
|
|
312
347
|
}
|
|
313
348
|
keep.push(f);
|
|
@@ -336,7 +371,7 @@
|
|
|
336
371
|
if (dirCount) {
|
|
337
372
|
r.rejected.push({
|
|
338
373
|
file: { name: dirCount + ' 个文件夹', size: 0 },
|
|
339
|
-
reason: 'dir', text:
|
|
374
|
+
reason: 'dir', text: this.labels.folder
|
|
340
375
|
});
|
|
341
376
|
}
|
|
342
377
|
|
|
@@ -344,7 +379,7 @@
|
|
|
344
379
|
/* 一个都没收:仍要把拒收项画出来,否则用户"拖了没反应" */
|
|
345
380
|
this.rejected = r.rejected;
|
|
346
381
|
this.render();
|
|
347
|
-
this.announce(r.rejected.length ?
|
|
382
|
+
this.announce(this.labels.none(r.rejected.length ? r.rejected[0].text : ''));
|
|
348
383
|
flEmit(this.root, 'fl-upload', {
|
|
349
384
|
files: currentFiles(this.input), added: 0, rejected: r.rejected
|
|
350
385
|
});
|
|
@@ -354,9 +389,9 @@
|
|
|
354
389
|
if (!writeFiles(this.input, r.keep)) {
|
|
355
390
|
/* 见文件头:宁可明说不可用,也不能装作收下了 */
|
|
356
391
|
this.rejected = [{ file: r.keep[0], reason: 'unsupported',
|
|
357
|
-
text:
|
|
392
|
+
text: this.labels.noDrop }];
|
|
358
393
|
this.render();
|
|
359
|
-
this.announce(
|
|
394
|
+
this.announce(this.labels.noDropShort);
|
|
360
395
|
return false;
|
|
361
396
|
}
|
|
362
397
|
|
|
@@ -441,7 +476,7 @@
|
|
|
441
476
|
/* 状态词给读屏(图标是 aria-hidden)—— 也是色盲用户的第二条线索 */
|
|
442
477
|
var sr = document.createElement('span');
|
|
443
478
|
sr.className = 'upload__status';
|
|
444
|
-
sr.textContent =
|
|
479
|
+
sr.textContent = this.labels[state] || '';
|
|
445
480
|
mark.appendChild(sr);
|
|
446
481
|
|
|
447
482
|
var name = document.createElement('span');
|
|
@@ -477,12 +512,12 @@
|
|
|
477
512
|
var rm = document.createElement('button');
|
|
478
513
|
rm.className = 'upload__remove';
|
|
479
514
|
rm.type = 'button';
|
|
480
|
-
rm.setAttribute('aria-label',
|
|
515
|
+
rm.setAttribute('aria-label', this.labels.remove(file.name || ''));
|
|
481
516
|
rm.appendChild(iconEl('remove'));
|
|
482
517
|
rm.addEventListener('click', function () { self.removeAt(index); });
|
|
483
518
|
li.appendChild(rm);
|
|
484
519
|
|
|
485
|
-
if (pct != null && pct >= 0) meta.textContent =
|
|
520
|
+
if (pct != null && pct >= 0) meta.textContent = self.labels.uploading(pct);
|
|
486
521
|
}
|
|
487
522
|
|
|
488
523
|
return li;
|
|
@@ -536,7 +571,7 @@
|
|
|
536
571
|
mark.appendChild(iconEl('done'));
|
|
537
572
|
var sr = document.createElement('span');
|
|
538
573
|
sr.className = 'upload__status';
|
|
539
|
-
sr.textContent =
|
|
574
|
+
sr.textContent = this.labels.done;
|
|
540
575
|
mark.appendChild(sr);
|
|
541
576
|
}
|
|
542
577
|
if (meta && f) meta.textContent = humanSize(f.size);
|
|
@@ -557,7 +592,7 @@
|
|
|
557
592
|
}
|
|
558
593
|
this.paintProgress(bar, pct);
|
|
559
594
|
/* 进度也写成文本:**信息不能只在图形里**,否则强制色/色盲下丢失 */
|
|
560
|
-
if (meta) meta.textContent =
|
|
595
|
+
if (meta) meta.textContent = this.labels.uploading(pct);
|
|
561
596
|
},
|
|
562
597
|
|
|
563
598
|
/* ================================================================ 移除 */
|
|
@@ -579,7 +614,7 @@
|
|
|
579
614
|
|
|
580
615
|
this.rebind(); /* 索引整体前移,旧闭包里的 i 已过期 */
|
|
581
616
|
this.syncCount();
|
|
582
|
-
this.announce(
|
|
617
|
+
this.announce(this.labels.removed(removed.name || '文件', rest.length));
|
|
583
618
|
flEmit(this.root, 'fl-upload', {
|
|
584
619
|
files: rest, added: 0, removed: removed, rejected: []
|
|
585
620
|
});
|
|
@@ -620,9 +655,7 @@
|
|
|
620
655
|
|
|
621
656
|
summary: function (added, rejected) {
|
|
622
657
|
var total = currentFiles(this.input).length;
|
|
623
|
-
|
|
624
|
-
if (rejected) s += ';' + rejected + ' 个未通过校验';
|
|
625
|
-
return s;
|
|
658
|
+
return this.labels.summary(added, total, rejected);
|
|
626
659
|
},
|
|
627
660
|
|
|
628
661
|
/* ================================================================ 销毁 */
|
|
@@ -180,7 +180,19 @@
|
|
|
180
180
|
height: 0; /* 🔴 hr 默认有 inset 边框,必须清掉 */
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/* 🔴 必须是**后代**选择器,不是 `> img` —— 和下面 `.prose a` 同一条教训。
|
|
184
|
+
*
|
|
185
|
+
* Markdown / CommonMark 渲染图片出来是 `<p><img></p>`,图片的父节点是 `<p>`,
|
|
186
|
+
* 不是 `.prose` 的直接子代 ⇒ `> img` **一条都匹配不上**,
|
|
187
|
+
* 于是 `max-width:100%` 从未生效。
|
|
188
|
+
* 实测(外部审计 F1):`04-recipes/longform/content.html` 里
|
|
189
|
+
* `<p><img width="600">`,375px 溢出 246px、320px 溢出 301px。
|
|
190
|
+
* 对"给用户和 AI 照抄的配方页"来说,这个缺陷会被原样复制出去。
|
|
191
|
+
*
|
|
192
|
+
* `> a` → `a` 早在 0.9 就踩过一次(段落里的链接落到浏览器默认蓝,
|
|
193
|
+
* 暗色对比度 1.91:1)。同一个坑换了标签又踩一次,所以这次把范围写宽:
|
|
194
|
+
* 正文里任何位置的图片都受同一条约束。 */
|
|
195
|
+
.prose img {
|
|
184
196
|
max-width: 100%;
|
|
185
197
|
height: auto;
|
|
186
198
|
display: block;
|
|
@@ -74,7 +74,7 @@ ul.demo-list li { margin: var(--sp-1) 0; }
|
|
|
74
74
|
<span class="dr-bar__title">筛选</span>
|
|
75
75
|
<button class="dr-tiny" type="button" tabindex="-1" aria-label="关闭">×</button>
|
|
76
76
|
</div>
|
|
77
|
-
<p class="cmp__why"
|
|
77
|
+
<p class="cmp__why">关闭是<strong>独立目标</strong>:抽屉滑进来之后,用户第一件事就是找它。24px 的叉在触屏上要瞄准,点偏了就点到了背后的内容。</p>
|
|
78
78
|
</div>
|
|
79
79
|
<div class="cmp__pane cmp__good" data-good="button.drawer__close|h|>=44">
|
|
80
80
|
<h3>✅ 本库:<code>drawer__close</code> 44px</h3>
|
|
@@ -201,8 +201,44 @@
|
|
|
201
201
|
bottom: 'drawer--bottom'
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
+
|
|
205
|
+
/* HTML 净化钩子(F6 / 0.26.0)。
|
|
206
|
+
🔴 默认**不改变任何行为**:没传 `sanitize` 也没有 `setSanitizer()` 时,
|
|
207
|
+
`opt.content` 仍然按"调用方已净化"处理(与 0.25.x 一致)。
|
|
208
|
+
⇒ 想接入 DOMPurify 这类库:
|
|
209
|
+
Drawer.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
|
|
210
|
+
或逐次传 `opt.sanitize` 覆盖(逐次优先于模块级)。
|
|
211
|
+
⚠️ 钩子只处理**字符串入口**;传 Node 的入口本来就绕过了 HTML 解析。 */
|
|
212
|
+
var SANITIZER = null;
|
|
213
|
+
|
|
214
|
+
function setSanitizer(fn) {
|
|
215
|
+
SANITIZER = (typeof fn === 'function') ? fn : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeHtml(html, perCall) {
|
|
219
|
+
var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
|
|
220
|
+
return fn ? String(fn(String(html))) : String(html);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时行为逐字不变。 */
|
|
224
|
+
var DEFAULT_LABELS = { dialog: '抽屉', close: '关闭' };
|
|
225
|
+
|
|
226
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
227
|
+
var flLabels = function (defaults, src) {
|
|
228
|
+
src = src || {};
|
|
229
|
+
var out = {};
|
|
230
|
+
var k;
|
|
231
|
+
for (k in defaults) {
|
|
232
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
233
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
204
239
|
function open(opt) {
|
|
205
240
|
opt = opt || {};
|
|
241
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
206
242
|
var place = opt.place || 'right';
|
|
207
243
|
var placeCls = PLACE_CLASS[place];
|
|
208
244
|
if (!placeCls) placeCls = PLACE_CLASS.right;
|
|
@@ -226,7 +262,7 @@
|
|
|
226
262
|
d.setAttribute('aria-modal', 'true');
|
|
227
263
|
if (!d.id) d.id = 'drawer-' + (new Date()).getTime();
|
|
228
264
|
if (opt.title) d.setAttribute('aria-labelledby', d.id + '-title');
|
|
229
|
-
else d.setAttribute('aria-label', opt.label ||
|
|
265
|
+
else d.setAttribute('aria-label', opt.label || L.dialog);
|
|
230
266
|
d.tabIndex = -1; /* 焦点陷阱的兜底落点 */
|
|
231
267
|
|
|
232
268
|
var head = document.createElement('div');
|
|
@@ -240,16 +276,18 @@
|
|
|
240
276
|
导致上面所有 close.addEventListener 绑在函数上(静默失败)。 */
|
|
241
277
|
closeBtn.className = 'drawer__close';
|
|
242
278
|
closeBtn.type = 'button';
|
|
243
|
-
closeBtn.setAttribute('aria-label',
|
|
279
|
+
closeBtn.setAttribute('aria-label', L.close);
|
|
244
280
|
closeBtn.textContent = '\u00D7';
|
|
245
281
|
head.appendChild(h);
|
|
246
282
|
head.appendChild(closeBtn);
|
|
247
283
|
|
|
248
284
|
var body = document.createElement('div');
|
|
249
285
|
body.className = 'drawer__body';
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
286
|
+
if (typeof opt.content === 'string') {
|
|
287
|
+
/* @html-sink raw-html — `opt.content` 是有意的 HTML 入口,调用方负责净化;
|
|
288
|
+
登记在 SECURITY.md「HTML 入口(sink)」。`sanitize` / `setSanitizer` 可挂钩。 */
|
|
289
|
+
body.innerHTML = sanitizeHtml(opt.content, opt.sanitize);
|
|
290
|
+
}
|
|
253
291
|
else if (opt.content) body.appendChild(opt.content);
|
|
254
292
|
|
|
255
293
|
d.appendChild(head);
|
|
@@ -374,5 +412,5 @@
|
|
|
374
412
|
return { el: d, close: close };
|
|
375
413
|
}
|
|
376
414
|
|
|
377
|
-
global.Drawer = { open: open };
|
|
415
|
+
global.Drawer = { open: open, setSanitizer: setSanitizer };
|
|
378
416
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -330,6 +330,26 @@
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
function create(root, opt) {
|
|
333
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
334
|
+
var DEFAULT_LABELS = {
|
|
335
|
+
menu: '菜单'
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
339
|
+
var flLabels = function (defaults, src) {
|
|
340
|
+
src = src || {};
|
|
341
|
+
var out = {};
|
|
342
|
+
var k;
|
|
343
|
+
for (k in defaults) {
|
|
344
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
345
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
};
|
|
350
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
351
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
352
|
+
|
|
333
353
|
opt = opt || {};
|
|
334
354
|
var btn = root.querySelector('[data-dd-btn]') || root.querySelector('button');
|
|
335
355
|
var menu = root.querySelector('[data-dd-menu]');
|
|
@@ -344,7 +364,7 @@
|
|
|
344
364
|
/* APG:菜单要么全 aria-hidden,要么全不设 —— 不能半吊子 */
|
|
345
365
|
menu.setAttribute('role', 'menu');
|
|
346
366
|
if (!menu.getAttribute('aria-label') && !menu.getAttribute('aria-labelledby')) {
|
|
347
|
-
menu.setAttribute('aria-label', opt.label ||
|
|
367
|
+
menu.setAttribute('aria-label', opt.label || L.menu);
|
|
348
368
|
}
|
|
349
369
|
Array.prototype.forEach.call(menu.querySelectorAll('li'), function (li) {
|
|
350
370
|
/* 🔴 修一个**静默失效**(实测发现):
|
|
@@ -289,7 +289,7 @@ body[data-scroll-locked] { overflow: hidden; }
|
|
|
289
289
|
隐藏遮罩会把弹窗里的字一起吞掉(静默截断)。实测:屏幕开着 dialog 时
|
|
290
290
|
打印,遮罩是 `position: fixed; inset: 0; rgba(21,24,28,.4)`,整页 40% 黑。
|
|
291
291
|
⚠️ 必须写在这个文件里:写进 tokens.css 会被这里同特异性的 `position: fixed`
|
|
292
|
-
盖掉 ——
|
|
292
|
+
盖掉 —— 规则写了、不生效、不报错(由 print-policy-gate L3 抓到)。
|
|
293
293
|
========================================================================= */
|
|
294
294
|
@media print {
|
|
295
295
|
.dialog-backdrop {
|