agentcss 0.25.6 → 0.26.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.
Files changed (56) hide show
  1. package/01-tokens/behavior/labels.js +44 -0
  2. package/02-primitives/combobox/combobox.js +26 -3
  3. package/02-primitives/date-range/date-range.js +27 -5
  4. package/02-primitives/popover/popover.js +22 -2
  5. package/02-primitives/select/select.js +23 -2
  6. package/02-primitives/upload/upload.js +50 -17
  7. package/03-patterns/content/content.css +13 -1
  8. package/03-patterns/content/demo.html +1 -1
  9. package/03-patterns/drawer/drawer.js +44 -6
  10. package/03-patterns/dropdown/dropdown.js +21 -1
  11. package/03-patterns/overlay/overlay.js +39 -2
  12. package/03-patterns/pagination/pagination.js +37 -10
  13. package/03-patterns/tooltip/tooltip.js +17 -2
  14. package/03-patterns/tree/tree.js +21 -1
  15. package/04-recipes/analysis-report/demo.html +1 -1
  16. package/04-recipes/data-showcase/demo.html +2 -2
  17. package/04-recipes/longform/content.html +989 -74
  18. package/04-recipes/longform/longform.html +3 -3
  19. package/04-recipes/report/demo.html +1 -1
  20. package/04-recipes/table/README.md +22 -0
  21. package/04-recipes/table/demo.html +6 -6
  22. package/09-assets/bar/bar.js +28 -4
  23. package/09-assets/model-viewer/model-viewer.js +44 -12
  24. package/09-assets/scientific-plot/demo.html +6 -6
  25. package/API.md +32 -0
  26. package/CHANGELOG.md +71 -0
  27. package/README.md +17 -0
  28. package/README.zh-CN.md +16 -0
  29. package/SECURITY.md +16 -4
  30. package/START-HERE.md +14 -14
  31. package/ai/components.json +9 -4
  32. package/ai/pdf.js +50 -8
  33. package/dist/01-tokens/behavior/labels.js +11 -0
  34. package/dist/02-primitives/combobox/combobox.js +25 -3
  35. package/dist/02-primitives/date-range/date-range.js +26 -5
  36. package/dist/02-primitives/popover/popover.js +14 -1
  37. package/dist/02-primitives/select/select.js +22 -2
  38. package/dist/02-primitives/upload/upload.js +47 -17
  39. package/dist/03-patterns/content/content.css +1 -1
  40. package/dist/03-patterns/drawer/drawer.js +32 -4
  41. package/dist/03-patterns/dropdown/dropdown.js +20 -1
  42. package/dist/03-patterns/overlay/overlay.js +25 -1
  43. package/dist/03-patterns/pagination/pagination.js +34 -10
  44. package/dist/03-patterns/tooltip/tooltip.js +14 -2
  45. package/dist/03-patterns/tree/tree.js +20 -1
  46. package/dist/09-assets/bar/bar.js +23 -4
  47. package/dist/09-assets/model-viewer/model-viewer.js +38 -12
  48. package/dist/COSTS.md +14 -14
  49. package/docs/ACCESSIBILITY.md +2 -2
  50. package/docs/L10N.md +92 -0
  51. package/docs/RELEASE-PROCESS.md +7 -1
  52. package/docs/VERIFY-RELEASE.md +7 -2
  53. package/index.html +1 -1
  54. package/llms-full.txt +2 -2
  55. package/package.json +3 -2
  56. package/types/index.d.ts +218 -13
package/ai/pdf.js CHANGED
@@ -75,6 +75,35 @@ const DEFAULT_MARGIN = '18mm 15mm';
75
75
  * **找不到就明确报错,不许静默降级**。
76
76
  * 漂移由 `05-audit/pdf-export-gate.js` 的 E8 交叉核对(两边都找到时必须是同一个文件)。
77
77
  */
78
+ /* Playwright 实际会用到的可执行文件名。只收这几个 —— 名字表越宽,
79
+ 越可能在缓存目录里认错文件(例如 README、脚本)。 */
80
+ const PW_EXE = [
81
+ 'chrome', 'chrome.exe',
82
+ 'chrome-headless-shell', 'chrome-headless-shell.exe',
83
+ 'headless_shell', 'headless_shell.exe',
84
+ 'Chromium', 'Google Chrome for Testing', 'Google Chrome for Testing.exe',
85
+ ];
86
+
87
+ /** 在 Playwright 的 chromium* 目录里按文件名找可执行文件(有界递归,depth 层) */
88
+ function scanForExe(dir, depth) {
89
+ let names;
90
+ try { names = fs.readdirSync(dir); } catch (e) { return null; }
91
+ const dirs = [];
92
+ for (const n of names) {
93
+ const p = path.join(dir, n);
94
+ let st;
95
+ try { st = fs.statSync(p); } catch (e) { continue; }
96
+ if (st.isDirectory()) { dirs.push(p); continue; }
97
+ if (PW_EXE.indexOf(n) !== -1) return p;
98
+ }
99
+ if (depth <= 0) return null;
100
+ for (const d of dirs) {
101
+ const hit = scanForExe(d, depth - 1);
102
+ if (hit) return hit;
103
+ }
104
+ return null;
105
+ }
106
+
78
107
  function chromeCandidates() {
79
108
  const out = [];
80
109
  if (process.env.AGENTCSS_CHROME) out.push(process.env.AGENTCSS_CHROME);
@@ -97,18 +126,28 @@ function chromeCandidates() {
97
126
  out.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
98
127
  '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
99
128
  }
100
- /* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"只在开发机上成立 */
101
- for (const root of [path.join(os.homedir(), '.cache/ms-playwright'),
102
- path.join(os.homedir(), 'Library/Caches/ms-playwright')]) {
129
+ /* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"只在开发机上成立。
130
+ ⚠️ **不写死目录布局**。Playwright 改过两次命名:
131
+ chrome-linux/chrome → chrome-linux64/chrome
132
+ chrome-linux/headless_shell → chrome-headless-shell-linux64/chrome-headless-shell
133
+ 写死布局的结果就是"某天起突然 no-chrome"(外部审计 F5 实测:
134
+ 干净环境里 `npm ci && npx playwright install chromium` 之后依然找不到)。
135
+ ⇒ 在 chromium* 目录下按**可执行文件名**有界递归找,布局怎么变都还能找到。
136
+ ⚠️ Windows 的根目录是 %LOCALAPPDATA%\ms-playwright —— `~/.cache` 在 Windows 上不存在。 */
137
+ const roots = [
138
+ process.env.PLAYWRIGHT_BROWSERS_PATH,
139
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'ms-playwright'),
140
+ path.join(os.homedir(), '.cache/ms-playwright'),
141
+ path.join(os.homedir(), 'Library/Caches/ms-playwright'),
142
+ ].filter(Boolean);
143
+ for (const root of roots) {
103
144
  let names;
104
145
  try { names = fs.readdirSync(root); } catch (e) { continue; }
105
146
  for (const n of names) {
106
147
  if (!/^chromium/.test(n)) continue;
107
- for (const rel of ['chrome-linux/chrome',
108
- 'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
109
- 'chrome-win/chrome.exe', 'chrome-linux/headless_shell']) {
110
- out.push(path.join(root, n, ...rel.split('/')));
111
- }
148
+ /* 深度 4:macOS 的 .app 包多两层(见 browser.js 同一处注释) */
149
+ const hit = scanForExe(path.join(root, n), 4);
150
+ if (hit) out.push(hit);
112
151
  }
113
152
  }
114
153
  return out.filter(Boolean);
@@ -351,4 +390,7 @@ function overBudgetMessage(got, budget, spec, margin, out, box, injected) {
351
390
  module.exports = {
352
391
  exportPdf, resolveChrome, normalizeMargin, paperSpec, PAPERS,
353
392
  DEFAULT_PAPER, DEFAULT_MARGIN,
393
+ /* 只为可测性导出:浏览器发现逻辑必须在"没有系统 Chrome"的机器上也能被验证,
394
+ 否则它只会在 CI 上坏、而开发机永远绿。 */
395
+ chromeCandidates,
354
396
  };
@@ -0,0 +1,11 @@
1
+ var flLabels = function (defaults, src) {
2
+ src = src || {};
3
+ var out = {};
4
+ var k;
5
+ for (k in defaults) {
6
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
7
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
8
+ }
9
+ }
10
+ return out;
11
+ };
@@ -119,6 +119,28 @@ return true;
119
119
  var uid = 0;
120
120
 
121
121
  function create(root, opt) {
122
+
123
+ var DEFAULT_LABELS = {
124
+ suggest: '建议',
125
+ input: '输入',
126
+ noMatch: function (q) { return '没有匹配「' + q + '」的选项'; },
127
+ empty: '暂无可选'
128
+ };
129
+
130
+ var flLabels = function (defaults, src) {
131
+ src = src || {};
132
+ var out = {};
133
+ var k;
134
+ for (k in defaults) {
135
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
136
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
137
+ }
138
+ }
139
+ return out;
140
+ };
141
+
142
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
143
+
122
144
  opt = opt || {};
123
145
  var input = root.querySelector('[data-combo-input]');
124
146
  var list = root.querySelector('[data-combo-list]');
@@ -147,14 +169,14 @@ var activeIdx = -1;
147
169
  var shown = [];
148
170
 
149
171
  list.setAttribute('role', 'listbox');
150
- list.setAttribute('aria-label', opt.label || '建议');
172
+ list.setAttribute('aria-label', opt.label || L.suggest);
151
173
  input.setAttribute('role', 'combobox');
152
174
  input.setAttribute('aria-autocomplete', 'list');
153
175
  input.setAttribute('aria-expanded', 'false');
154
176
  input.setAttribute('aria-controls', list.id);
155
177
  if (!input.id) input.id = 'combo-input-' + seq;
156
178
  if (!input.getAttribute('aria-label') && !root.querySelector('label[for="' + input.id + '"]')) {
157
- input.setAttribute('aria-label', opt.label || '输入');
179
+ input.setAttribute('aria-label', opt.label || L.input);
158
180
  }
159
181
 
160
182
  function esc(s) {
@@ -190,7 +212,7 @@ return String(s).replace(/[&<>"]/g, function (c) {
190
212
  if (!shown.length) {
191
213
  /* @html-sink escaped — 插值只有 q,且过了 esc() */
192
214
  list.innerHTML = '<li class="combo__empty" role="presentation">' +
193
- (q ? '没有匹配「' + esc(q) + '」的选项' : '暂无可选') + '</li>';
215
+ (q ? L.noMatch(esc(q)) : L.empty) + '</li>';
194
216
  return;
195
217
  }
196
218
  var ql = q.toLowerCase();
@@ -94,6 +94,27 @@ return { from: iso(start), to: iso(end) };
94
94
  }
95
95
 
96
96
  function create(root, opt) {
97
+
98
+ var flLabels = function (defaults, src) {
99
+ src = src || {};
100
+ var out = {};
101
+ var k;
102
+ for (k in defaults) {
103
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
104
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
105
+ }
106
+ }
107
+ return out;
108
+ };
109
+
110
+ var DEFAULT_LABELS = {
111
+ requiredEnd: '请填写结束日期。',
112
+ requiredStart: '请填写开始日期。',
113
+ order: function (f, t) { return '开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。'; },
114
+ min: function (min) { return '开始日期不能早于 ' + min + '。'; },
115
+ max: function (max) { return '结束日期不能晚于 ' + max + '。'; }
116
+ };
117
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
97
118
  opt = opt || {};
98
119
  var from = root.querySelector('[data-dr-from]');
99
120
  var to = root.querySelector('[data-dr-to]');
@@ -132,14 +153,14 @@ return Math.round((b - a) / 86400000) + 1;
132
153
  function validate() {
133
154
  var f = from.value, t = to.value;
134
155
  if (!f && !t) { setError(''); paintSummary(''); return true; }
135
- if (f && !t) { setError('请填写结束日期。'); return false; }
136
- if (!f && t) { setError('请填写开始日期。'); return false; }
156
+ if (f && !t) { setError(L.requiredEnd); return false; }
157
+ if (!f && t) { setError(L.requiredStart); return false; }
137
158
  if (f > t) {
138
- setError('开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。');
159
+ setError(L.order(f, t));
139
160
  return false;
140
161
  }
141
- if (opt.min && f < opt.min) { setError('开始日期不能早于 ' + opt.min + '。'); return false; }
142
- if (opt.max && t > opt.max) { setError('结束日期不能晚于 ' + opt.max + '。'); return false; }
162
+ if (opt.min && f < opt.min) { setError(L.min(opt.min)); return false; }
163
+ if (opt.max && t > opt.max) { setError(L.max(opt.max)); return false; }
143
164
  setError('');
144
165
  paintSummary(f + ' → ' + t + '(' + days(f, t) + ' 天)');
145
166
  return true;
@@ -148,10 +148,11 @@ box.setAttribute('inert', '');
148
148
 
149
149
  box.setAttribute('role', 'dialog');
150
150
  box.setAttribute('aria-label', opts.label || '');
151
+ var bodyHtml = sanitizeHtml(opts.html || '', opts.sanitize);
151
152
 
152
153
  box.innerHTML =
153
154
  '<span class="popover__arrow" aria-hidden="true"></span>'+
154
- '<div class="popover__body">'+ (opts.html || '') + '</div>';
155
+ '<div class="popover__body">'+ bodyHtml + '</div>';
155
156
 
156
157
  positionArrow(box, trigger);
157
158
  anchor.appendChild(box);
@@ -230,6 +231,17 @@ el: box,
230
231
  return api;
231
232
  }
232
233
 
234
+ var SANITIZER = null;
235
+
236
+ function setSanitizer(fn) {
237
+ SANITIZER = (typeof fn === 'function') ? fn : null;
238
+ }
239
+
240
+ function sanitizeHtml(html, perCall) {
241
+ var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
242
+ return fn ? String(fn(String(html))) : String(html);
243
+ }
244
+
233
245
  function positionArrow(box, trigger) {
234
246
  var arrow = box.querySelector('.popover__arrow');
235
247
  if (!arrow) return;
@@ -256,5 +268,6 @@ closeAll: function () { if (current) current.close(); },
256
268
  get current() { return current; },
257
269
  };
258
270
 
271
+ Popover.setSanitizer = setSanitizer;
259
272
  global.Popover = Popover;
260
273
  })(typeof window !== 'undefined' ? window : globalThis);
@@ -213,6 +213,26 @@ return true;
213
213
  var uid = 0;
214
214
 
215
215
  function create(root, opt) {
216
+
217
+ var DEFAULT_LABELS = {
218
+ option: '选项',
219
+ placeholder: '请选择'
220
+ };
221
+
222
+ var flLabels = function (defaults, src) {
223
+ src = src || {};
224
+ var out = {};
225
+ var k;
226
+ for (k in defaults) {
227
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
228
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
229
+ }
230
+ }
231
+ return out;
232
+ };
233
+
234
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
235
+
216
236
  opt = opt || {};
217
237
  var btn = root.querySelector('[data-select-btn]');
218
238
  var list = root.querySelector('[data-select-list]');
@@ -253,7 +273,7 @@ seed(opt.value);
253
273
 
254
274
  list.setAttribute('role', 'listbox');
255
275
  if (isMulti) list.setAttribute('aria-multiselectable', 'true');
256
- list.setAttribute('aria-label', opt.label || '选项');
276
+ list.setAttribute('aria-label', opt.label || L.option);
257
277
  btn.setAttribute('aria-haspopup', 'listbox');
258
278
  btn.setAttribute('aria-expanded', 'false');
259
279
  btn.setAttribute('aria-controls', list.id);
@@ -293,7 +313,7 @@ var texts = [];
293
313
  for (var i = 0; i < options.length; i++) {
294
314
  if (hasValue(valueOfOpt(options[i]))) texts.push(optText(options[i]));
295
315
  }
296
- return texts.length ? texts.join('、') : (opt.placeholder || '请选择');
316
+ return texts.length ? texts.join('、') : (opt.placeholder || L.placeholder);
297
317
  }
298
318
 
299
319
  function optText(o) {
@@ -32,7 +32,38 @@ error: 'M8 1.7a6.3 6.3 0 1 0 0 12.6A6.3 6.3 0 0 0 8 1.7zM8 4.9v3.4M8 11.1v.5',
32
32
  remove: 'M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6'
33
33
  };
34
34
 
35
- var STATE_TEXT = { pending: '待上传', done: '已完成', error: '失败' };
35
+ var DEFAULT_LABELS = {
36
+ pending: '待上传',
37
+ done: '已完成',
38
+ error: '失败',
39
+ remove: function (name) { return '移除 ' + name; },
40
+ uploading: function (pct) { return '上传中 ' + Math.round(pct) + '%'; },
41
+ countMax: function (n) { return '最多 ' + n + ' 个文件'; },
42
+ sizeMax: function (limit) { return '超过 ' + limit; },
43
+ type: '类型不支持',
44
+ folder: '请拖入文件,不支持文件夹',
45
+ none: function (reason) { return reason ? '未添加任何文件:' + reason : '未添加任何文件'; },
46
+ noDrop: '当前浏览器不支持拖放,请点「选择文件」',
47
+ noDropShort: '当前浏览器不支持拖放,请点选择文件',
48
+ summary: function (added, total, rejected) {
49
+ var s = '已添加 ' + added + ' 个文件,共 ' + total + ' 个';
50
+ if (rejected) s += ';' + rejected + ' 个未通过校验';
51
+ return s;
52
+ },
53
+ removed: function (name, rest) { return '已移除 ' + name + ',还剩 ' + rest + ' 个'; }
54
+ };
55
+
56
+ var flLabels = function (defaults, src) {
57
+ src = src || {};
58
+ var out = {};
59
+ var k;
60
+ for (k in defaults) {
61
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
62
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
63
+ }
64
+ }
65
+ return out;
66
+ };
36
67
 
37
68
  function humanSize(n) {
38
69
  if (typeof n !== 'number' || n < 0) return '';
@@ -110,6 +141,7 @@ function Upload(root, opts) {
110
141
  var opt = opts || {};
111
142
 
112
143
  this.root = root;
144
+ this.labels = flLabels(DEFAULT_LABELS, opt && opt.labels);
113
145
  this.input = root.querySelector('.upload__input');
114
146
  this.list = root.querySelector('.upload__list');
115
147
  this.status = root.querySelector('.upload__status');
@@ -209,15 +241,15 @@ var accept = this.input.getAttribute('accept');
209
241
  for (var i = 0; i < incoming.length; i++) {
210
242
  var f = incoming[i];
211
243
  if (maxFiles && keep.length >= maxFiles) {
212
- rejected.push({ file: f, reason: 'count', text: '最多 ' + maxFiles + ' 个文件' });
244
+ rejected.push({ file: f, reason: 'count', text: this.labels.countMax(maxFiles) });
213
245
  continue;
214
246
  }
215
247
  if (this.maxSize && f.size > this.maxSize) {
216
- rejected.push({ file: f, reason: 'size', text: '超过 ' + humanSize(this.maxSize) });
248
+ rejected.push({ file: f, reason: 'size', text: this.labels.sizeMax(humanSize(this.maxSize)) });
217
249
  continue;
218
250
  }
219
251
  if (!matchesAccept(f, accept)) {
220
- rejected.push({ file: f, reason: 'type', text: '类型不支持' });
252
+ rejected.push({ file: f, reason: 'type', text: this.labels.type });
221
253
  continue;
222
254
  }
223
255
  keep.push(f);
@@ -231,7 +263,7 @@ var r = this._validate(incoming);
231
263
  if (dirCount) {
232
264
  r.rejected.push({
233
265
  file: { name: dirCount + ' 个文件夹', size: 0 },
234
- reason: 'dir', text: '请拖入文件,不支持文件夹'
266
+ reason: 'dir', text: this.labels.folder
235
267
  });
236
268
  }
237
269
 
@@ -239,7 +271,7 @@ if (!r.keep.length) {
239
271
 
240
272
  this.rejected = r.rejected;
241
273
  this.render();
242
- this.announce(r.rejected.length ? '未添加任何文件:' + r.rejected[0].text : '未添加任何文件');
274
+ this.announce(this.labels.none(r.rejected.length ? r.rejected[0].text : ''));
243
275
  flEmit(this.root, 'fl-upload', {
244
276
  files: currentFiles(this.input), added: 0, rejected: r.rejected
245
277
  });
@@ -249,9 +281,9 @@ return false;
249
281
  if (!writeFiles(this.input, r.keep)) {
250
282
 
251
283
  this.rejected = [{ file: r.keep[0], reason: 'unsupported',
252
- text: '当前浏览器不支持拖放,请点「选择文件」' }];
284
+ text: this.labels.noDrop }];
253
285
  this.render();
254
- this.announce('当前浏览器不支持拖放,请点选择文件');
286
+ this.announce(this.labels.noDropShort);
255
287
  return false;
256
288
  }
257
289
 
@@ -317,7 +349,7 @@ mark.appendChild(iconEl(state === 'done' ? 'done' : state === 'error' ? 'error'
317
349
 
318
350
  var sr = document.createElement('span');
319
351
  sr.className = 'upload__status';
320
- sr.textContent = STATE_TEXT[state] || '';
352
+ sr.textContent = this.labels[state] || '';
321
353
  mark.appendChild(sr);
322
354
 
323
355
  var name = document.createElement('span');
@@ -346,12 +378,12 @@ this.paintProgress(bar0, pct);
346
378
  var rm = document.createElement('button');
347
379
  rm.className = 'upload__remove';
348
380
  rm.type = 'button';
349
- rm.setAttribute('aria-label', '移除 ' + (file.name || ''));
381
+ rm.setAttribute('aria-label', this.labels.remove(file.name || ''));
350
382
  rm.appendChild(iconEl('remove'));
351
383
  rm.addEventListener('click', function () { self.removeAt(index); });
352
384
  li.appendChild(rm);
353
385
 
354
- if (pct != null && pct >= 0) meta.textContent = '上传中 ' + Math.round(pct) + '%';
386
+ if (pct != null && pct >= 0) meta.textContent = self.labels.uploading(pct);
355
387
  }
356
388
 
357
389
  return li;
@@ -396,7 +428,7 @@ mark.innerHTML = '';
396
428
  mark.appendChild(iconEl('done'));
397
429
  var sr = document.createElement('span');
398
430
  sr.className = 'upload__status';
399
- sr.textContent = STATE_TEXT.done;
431
+ sr.textContent = this.labels.done;
400
432
  mark.appendChild(sr);
401
433
  }
402
434
  if (meta && f) meta.textContent = humanSize(f.size);
@@ -417,7 +449,7 @@ li.insertBefore(bar, rmBtn || null);
417
449
  }
418
450
  this.paintProgress(bar, pct);
419
451
 
420
- if (meta) meta.textContent = '上传中 ' + Math.round(pct) + '%';
452
+ if (meta) meta.textContent = this.labels.uploading(pct);
421
453
  },
422
454
 
423
455
  removeAt: function (i) {
@@ -435,7 +467,7 @@ if (li && li.parentNode) li.parentNode.removeChild(li);
435
467
 
436
468
  this.rebind();
437
469
  this.syncCount();
438
- this.announce('已移除 ' + (removed.name || '文件') + ',还剩 ' + rest.length + ' 个');
470
+ this.announce(this.labels.removed(removed.name || '文件', rest.length));
439
471
  flEmit(this.root, 'fl-upload', {
440
472
  files: rest, added: 0, removed: removed, rejected: []
441
473
  });
@@ -467,9 +499,7 @@ this._announceTimer = global.setTimeout(function () { el.textContent = msg; }, 6
467
499
 
468
500
  summary: function (added, rejected) {
469
501
  var total = currentFiles(this.input).length;
470
- var s = '已添加 ' + added + ' 个文件,共 ' + total + ' 个';
471
- if (rejected) s += ';' + rejected + ' 个未通过校验';
472
- return s;
502
+ return this.labels.summary(added, total, rejected);
473
503
  },
474
504
 
475
505
  destroy: function () {
@@ -1 +1 @@
1
- .prose {max-width:var(--measure-prose);color:var(--text-primary);}.prose > * + * {margin-top:var(--sp-4);}.prose > h1, .prose > h2, .prose > h3, .prose > h4, .prose > h5, .prose > h6 {font-family:var(--sans);font-weight:500;line-height:var(--lh-tight);letter-spacing:-0.008em;margin:var(--sp-7) 0 0;}.prose > h1 {font-size:var(--fs-2xl);margin-top:0;}.prose > h2 {font-size:var(--fs-lg);padding-top:var(--sp-5);border-top:2px solid var(--accent);max-width:92%;}.prose > h3 {font-size:var(--fs-md);}.prose > h4, .prose > h5, .prose > h6 {font-size:var(--fs-base);color:var(--text-secondary);}.prose > p {margin:0;line-height:1.75;}.prose > code, .prose > li > code, .prose > td > code, .prose > th > code {font-family:var(--mono);font-size:0.88em;background:var(--surface-sunken);border:1px solid var(--border-decor);border-radius:3px;padding:0.05em 0.3em;}.prose > pre > code, .prose > pre {font-family:inherit;font-size:inherit;background:none;border:none;padding:0;border-radius:0;}.prose > ul, .prose > ol {padding-inline-start:1.5em;margin:0;}.prose > ul {list-style:disc;}.prose > ol {list-style:decimal;}.prose li + li {margin-top:var(--sp-2);}.prose li > ul, .prose li > ol {margin-top:var(--sp-2);}.prose li > ul {list-style:circle;}.prose > blockquote {margin:var(--sp-5) 0;padding:0 0 0 var(--sp-4);border-inline-start:3px solid var(--accent);}.prose > blockquote > p {color:var(--text-secondary);font-size:var(--fs-md);line-height:1.6;}.prose > blockquote > blockquote {margin:var(--sp-3) 0;}.prose > pre {margin:0;padding:var(--sp-4);background:var(--surface-sunken);border-inline-start:3px solid var(--border-decor-str);border-radius:0 var(--r-sm) var(--r-sm) 0;overflow-x:auto;font-family:var(--mono);font-size:var(--fs-xs);line-height:1.6;tab-size:4;-webkit-overflow-scrolling:touch;}.prose > pre:not([class]) {border-inline-start-color:var(--border-decor);}.prose > table {width:100%;border-collapse:collapse;table-layout:auto;font-size:var(--fs-sm);}.prose > table th, .prose > table td {text-align:start;padding:var(--sp-2) var(--sp-4) var(--sp-2) 0;border-bottom:1px solid var(--border-decor);vertical-align:top;}.prose > table th {font-weight:500;font-size:var(--fs-xs);color:var(--text-secondary);}.prose > table tbody tr:last-child td {border-bottom:1px solid var(--border-decor-str);}.prose > hr {margin:var(--sp-7) 0;border:0;border-top:1px solid var(--border-decor);height:0;}.prose > img {max-width:100%;height:auto;display:block;border-radius:var(--r-sm);}.prose a {color:var(--accent);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;}@media (hover:hover) and (pointer:fine) {.prose a:hover {color:var(--text-primary);}}.prose a:focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.prose > a[target="_blank"]::after {content:" ↗";font-size:0.8em;vertical-align:0.1em;}.prose > del, .prose > s, .prose > li > del, .prose > li > s {color:var(--text-tertiary);}.prose .task-list-item {list-style:none;margin-inline-start:-1.5em;}.prose .task-list-item input[type="checkbox"] {margin-inline-end:var(--sp-2);vertical-align:middle;}.prose .task-list-item input[disabled] {accent-color:var(--accent);}.prose :focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.prose a:visited {color:var(--accent);text-decoration:underline;text-underline-offset:2px;}
1
+ .prose {max-width:var(--measure-prose);color:var(--text-primary);}.prose > * + * {margin-top:var(--sp-4);}.prose > h1, .prose > h2, .prose > h3, .prose > h4, .prose > h5, .prose > h6 {font-family:var(--sans);font-weight:500;line-height:var(--lh-tight);letter-spacing:-0.008em;margin:var(--sp-7) 0 0;}.prose > h1 {font-size:var(--fs-2xl);margin-top:0;}.prose > h2 {font-size:var(--fs-lg);padding-top:var(--sp-5);border-top:2px solid var(--accent);max-width:92%;}.prose > h3 {font-size:var(--fs-md);}.prose > h4, .prose > h5, .prose > h6 {font-size:var(--fs-base);color:var(--text-secondary);}.prose > p {margin:0;line-height:1.75;}.prose > code, .prose > li > code, .prose > td > code, .prose > th > code {font-family:var(--mono);font-size:0.88em;background:var(--surface-sunken);border:1px solid var(--border-decor);border-radius:3px;padding:0.05em 0.3em;}.prose > pre > code, .prose > pre {font-family:inherit;font-size:inherit;background:none;border:none;padding:0;border-radius:0;}.prose > ul, .prose > ol {padding-inline-start:1.5em;margin:0;}.prose > ul {list-style:disc;}.prose > ol {list-style:decimal;}.prose li + li {margin-top:var(--sp-2);}.prose li > ul, .prose li > ol {margin-top:var(--sp-2);}.prose li > ul {list-style:circle;}.prose > blockquote {margin:var(--sp-5) 0;padding:0 0 0 var(--sp-4);border-inline-start:3px solid var(--accent);}.prose > blockquote > p {color:var(--text-secondary);font-size:var(--fs-md);line-height:1.6;}.prose > blockquote > blockquote {margin:var(--sp-3) 0;}.prose > pre {margin:0;padding:var(--sp-4);background:var(--surface-sunken);border-inline-start:3px solid var(--border-decor-str);border-radius:0 var(--r-sm) var(--r-sm) 0;overflow-x:auto;font-family:var(--mono);font-size:var(--fs-xs);line-height:1.6;tab-size:4;-webkit-overflow-scrolling:touch;}.prose > pre:not([class]) {border-inline-start-color:var(--border-decor);}.prose > table {width:100%;border-collapse:collapse;table-layout:auto;font-size:var(--fs-sm);}.prose > table th, .prose > table td {text-align:start;padding:var(--sp-2) var(--sp-4) var(--sp-2) 0;border-bottom:1px solid var(--border-decor);vertical-align:top;}.prose > table th {font-weight:500;font-size:var(--fs-xs);color:var(--text-secondary);}.prose > table tbody tr:last-child td {border-bottom:1px solid var(--border-decor-str);}.prose > hr {margin:var(--sp-7) 0;border:0;border-top:1px solid var(--border-decor);height:0;}.prose img {max-width:100%;height:auto;display:block;border-radius:var(--r-sm);}.prose a {color:var(--accent);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;}@media (hover:hover) and (pointer:fine) {.prose a:hover {color:var(--text-primary);}}.prose a:focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.prose > a[target="_blank"]::after {content:" ↗";font-size:0.8em;vertical-align:0.1em;}.prose > del, .prose > s, .prose > li > del, .prose > li > s {color:var(--text-tertiary);}.prose .task-list-item {list-style:none;margin-inline-start:-1.5em;}.prose .task-list-item input[type="checkbox"] {margin-inline-end:var(--sp-2);vertical-align:middle;}.prose .task-list-item input[disabled] {accent-color:var(--accent);}.prose :focus-visible {outline:2px solid var(--accent);outline-offset:2px;}.prose a:visited {color:var(--accent);text-decoration:underline;text-underline-offset:2px;}
@@ -171,8 +171,34 @@ top: 'drawer--top',
171
171
  bottom: 'drawer--bottom'
172
172
  };
173
173
 
174
+ var SANITIZER = null;
175
+
176
+ function setSanitizer(fn) {
177
+ SANITIZER = (typeof fn === 'function') ? fn : null;
178
+ }
179
+
180
+ function sanitizeHtml(html, perCall) {
181
+ var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
182
+ return fn ? String(fn(String(html))) : String(html);
183
+ }
184
+
185
+ var DEFAULT_LABELS = { dialog: '抽屉', close: '关闭' };
186
+
187
+ var flLabels = function (defaults, src) {
188
+ src = src || {};
189
+ var out = {};
190
+ var k;
191
+ for (k in defaults) {
192
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
193
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
194
+ }
195
+ }
196
+ return out;
197
+ };
198
+
174
199
  function open(opt) {
175
200
  opt = opt || {};
201
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
176
202
  var place = opt.place || 'right';
177
203
  var placeCls = PLACE_CLASS[place];
178
204
  if (!placeCls) placeCls = PLACE_CLASS.right;
@@ -191,7 +217,7 @@ d.setAttribute('role', 'dialog');
191
217
  d.setAttribute('aria-modal', 'true');
192
218
  if (!d.id) d.id = 'drawer-' + (new Date()).getTime();
193
219
  if (opt.title) d.setAttribute('aria-labelledby', d.id + '-title');
194
- else d.setAttribute('aria-label', opt.label || '抽屉');
220
+ else d.setAttribute('aria-label', opt.label || L.dialog);
195
221
  d.tabIndex = -1;
196
222
 
197
223
  var head = document.createElement('div');
@@ -203,15 +229,17 @@ h.textContent = opt.title || '';
203
229
  var closeBtn = document.createElement('button');
204
230
  closeBtn.className = 'drawer__close';
205
231
  closeBtn.type = 'button';
206
- closeBtn.setAttribute('aria-label', '关闭');
232
+ closeBtn.setAttribute('aria-label', L.close);
207
233
  closeBtn.textContent = '\u00D7';
208
234
  head.appendChild(h);
209
235
  head.appendChild(closeBtn);
210
236
 
211
237
  var body = document.createElement('div');
212
238
  body.className = 'drawer__body';
239
+ if (typeof opt.content === 'string') {
213
240
 
214
- if (typeof opt.content === 'string') body.innerHTML = opt.content;
241
+ body.innerHTML = sanitizeHtml(opt.content, opt.sanitize);
242
+ }
215
243
  else if (opt.content) body.appendChild(opt.content);
216
244
 
217
245
  d.appendChild(head);
@@ -311,5 +339,5 @@ if (opt.onClose) opt.onClose();
311
339
  return { el: d, close: close };
312
340
  }
313
341
 
314
- global.Drawer = { open: open };
342
+ global.Drawer = { open: open, setSanitizer: setSanitizer };
315
343
  })(typeof window !== 'undefined' ? window : globalThis);
@@ -295,6 +295,25 @@ return Array.prototype.slice.call(menu.querySelectorAll(FOCUSABLE));
295
295
  }
296
296
 
297
297
  function create(root, opt) {
298
+
299
+ var DEFAULT_LABELS = {
300
+ menu: '菜单'
301
+ };
302
+
303
+ var flLabels = function (defaults, src) {
304
+ src = src || {};
305
+ var out = {};
306
+ var k;
307
+ for (k in defaults) {
308
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
309
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
310
+ }
311
+ }
312
+ return out;
313
+ };
314
+
315
+ var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
316
+
298
317
  opt = opt || {};
299
318
  var btn = root.querySelector('[data-dd-btn]') || root.querySelector('button');
300
319
  var menu = root.querySelector('[data-dd-menu]');
@@ -305,7 +324,7 @@ var active = -1;
305
324
 
306
325
  menu.setAttribute('role', 'menu');
307
326
  if (!menu.getAttribute('aria-label') && !menu.getAttribute('aria-labelledby')) {
308
- menu.setAttribute('aria-label', opt.label || '菜单');
327
+ menu.setAttribute('aria-label', opt.label || L.menu);
309
328
  }
310
329
  Array.prototype.forEach.call(menu.querySelectorAll('li'), function (li) {
311
330
 
@@ -156,8 +156,27 @@ error: 'toast--error',
156
156
  warning: 'toast--warning'
157
157
  };
158
158
 
159
+ var DEFAULT_LABELS = {
160
+ closeToast: '关闭提示'
161
+ };
162
+
163
+ var flLabels = function (defaults, src) {
164
+ src = src || {};
165
+ var out = {};
166
+ var k;
167
+ for (k in defaults) {
168
+ if (Object.prototype.hasOwnProperty.call(defaults, k)) {
169
+ out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
170
+ }
171
+ }
172
+ return out;
173
+ };
174
+
175
+ var TOAST_LIMIT = 4;
176
+
159
177
  function toast(opts) {
160
178
  opts = opts || {};
179
+ var L = flLabels(DEFAULT_LABELS, opts && opts.labels);
161
180
 
162
181
  var region = document.querySelector('.toast-region');
163
182
  if (!region) {
@@ -203,7 +222,7 @@ el.appendChild(timer);
203
222
  var closeBtn = document.createElement('button');
204
223
  closeBtn.className = 'toast__close';
205
224
  closeBtn.type = 'button';
206
- closeBtn.setAttribute('aria-label', '关闭提示');
225
+ closeBtn.setAttribute('aria-label', L.closeToast);
207
226
  closeBtn.textContent = '×';
208
227
  el.appendChild(closeBtn);
209
228
 
@@ -220,6 +239,11 @@ if (el.parentNode) el.parentNode.removeChild(el);
220
239
  }
221
240
 
222
241
  closeBtn.addEventListener('click', close);
242
+
243
+ var max = (opts.maxVisible === undefined) ? TOAST_LIMIT : opts.maxVisible;
244
+ while (max > 0 && region.children.length >= max) {
245
+ region.removeChild(region.firstChild);
246
+ }
223
247
  region.appendChild(el);
224
248
 
225
249
  var timerId = auto > 0 ? setTimeout(close, auto) : null;