agentcss 0.25.5 → 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 (59) 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 +129 -0
  27. package/CONTRIBUTING.md +4 -3
  28. package/README.md +19 -2
  29. package/README.zh-CN.md +18 -2
  30. package/SECURITY.md +16 -4
  31. package/START-HERE.md +14 -14
  32. package/SUPPORT.md +2 -2
  33. package/ai/components.json +9 -4
  34. package/ai/pdf.js +50 -8
  35. package/dist/01-tokens/behavior/labels.js +11 -0
  36. package/dist/02-primitives/combobox/combobox.js +25 -3
  37. package/dist/02-primitives/date-range/date-range.js +26 -5
  38. package/dist/02-primitives/popover/popover.js +14 -1
  39. package/dist/02-primitives/select/select.js +22 -2
  40. package/dist/02-primitives/upload/upload.js +47 -17
  41. package/dist/03-patterns/content/content.css +1 -1
  42. package/dist/03-patterns/drawer/drawer.js +32 -4
  43. package/dist/03-patterns/dropdown/dropdown.js +20 -1
  44. package/dist/03-patterns/overlay/overlay.js +25 -1
  45. package/dist/03-patterns/pagination/pagination.js +34 -10
  46. package/dist/03-patterns/tooltip/tooltip.js +14 -2
  47. package/dist/03-patterns/tree/tree.js +20 -1
  48. package/dist/09-assets/bar/bar.js +23 -4
  49. package/dist/09-assets/model-viewer/model-viewer.js +38 -12
  50. package/dist/COSTS.md +14 -14
  51. package/docs/ACCESSIBILITY.md +2 -2
  52. package/docs/L10N.md +92 -0
  53. package/docs/LAYER-BASELINE.md +2 -2
  54. package/docs/RELEASE-PROCESS.md +7 -1
  55. package/docs/VERIFY-RELEASE.md +7 -2
  56. package/index.html +1 -1
  57. package/llms-full.txt +2 -2
  58. package/package.json +17 -6
  59. package/types/index.d.ts +218 -13
package/START-HERE.md CHANGED
@@ -39,39 +39,39 @@
39
39
  | 标签 / 徽章 | `02-primitives/badge/badge.css` | 141 | — | 有状态标记就要("已披露""停牌") |
40
40
  | 卡片 | `02-primitives/card/card.css` | 283 | — | 有分组内容就要 |
41
41
  | 单选 / 多选 | `02-primitives/choice/choice.css` | 434 | — | 单选 / 多选就要 |
42
- | 下拉选择 | `02-primitives/select/select.css` | 151 | **必需 631** (`select.js`) | 原生 select 的外观或交互不够用就要 |
43
- | 可搜索选择 | `02-primitives/combobox/combobox.css` | 223 | **必需 480** (`combobox.js`) | 选项多到要搜索,或要选多个并留成标签就要 |
42
+ | 下拉选择 | `02-primitives/select/select.css` | 151 | **必需 652** (`select.js`) | 原生 select 的外观或交互不够用就要 |
43
+ | 可搜索选择 | `02-primitives/combobox/combobox.css` | 223 | **必需 503** (`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 | **必需 325** (`popover.js`) | 要一段信息贴着触发元素浮出来就要 |
49
- | 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 310** (`date-range.js`) | 要选起止日期就要 |
48
+ | 浮层气泡 | `02-primitives/popover/popover.css` | 187 | **必需 345** (`popover.js`) | 要一段信息贴着触发元素浮出来就要 |
49
+ | 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 332** (`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
- | 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 555** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
- | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 284** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
- | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
55
- | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
56
- | 侧边抽屉 | `03-patterns/drawer/drawer.css` | 145 | **必需 378** (`drawer.js`) | 要从侧边滑出面板就要 |
57
- | 弹窗 / 吐司 | `03-patterns/overlay/overlay.css` | 338 | **必需 697** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
52
+ | 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 575** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
+ | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 299** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
+ | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 428** (`pagination.js`) | 数据多到要翻页就要 |
55
+ | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 484** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
56
+ | 侧边抽屉 | `03-patterns/drawer/drawer.css` | 145 | **必需 416** (`drawer.js`) | 要从侧边滑出面板就要 |
57
+ | 弹窗 / 吐司 | `03-patterns/overlay/overlay.css` | 338 | **必需 734** (`overlay.js`) | 需要弹层(吐司 / 模态)就要 |
58
58
  | 状态(空 / 加载 / 错 / 成功) | `03-patterns/states/states.css` | 269 | — | **凡是会异步取数就要** |
59
59
  | 表单校验 | `03-patterns/form-validation/form-validation.css` | 274 | — | 提交前要校验就要 |
60
60
  | 列表增删 | `03-patterns/list/list.css` | 162 | 可选 201 (`flip.js`,FLIP 增删动画,纯增强) | 列表会动态增删就要 |
61
61
  | 导航 / 目录 | `03-patterns/nav/nav.css` | 314 | **必需 425** (`nav.js`) · 可选 111 (`toc.js`,自动生成目录,纯增强) | 顶部要导航,或长文要目录就要 |
62
- | 长文排版 | `03-patterns/content/content.css` | 286 | — | 写文档 / 长文就要 |
62
+ | 长文排版 | `03-patterns/content/content.css` | 298 | — | 写文档 / 长文就要 |
63
63
  | delta | `02-primitives/delta/delta.css` | 113 | — | |
64
64
  | metric | `02-primitives/metric/metric.css` | 157 | — | |
65
65
  | timeline | `02-primitives/timeline/timeline.css` | 152 | — | 有一串按时间排的事件(公告 / 大事记 / 版本记录)就要 |
66
- | upload | `02-primitives/upload/upload.css` | 490 | 可选 676 (`upload.js`,拖放 / 文件列表 / 校验;没有它基础形态照常工作) | |
66
+ | upload | `02-primitives/upload/upload.css` | 490 | 可选 709 (`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
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
- | bar | `09-assets/bar/bar.css` | 181 | **必需 324** (`bar.js`) | |
74
- | model-viewer | `09-assets/model-viewer/model-viewer.css` | 143 | **必需 432** (`model-viewer.js`) | |
73
+ | bar | `09-assets/bar/bar.css` | 181 | **必需 348** (`bar.js`) | |
74
+ | model-viewer | `09-assets/model-viewer/model-viewer.css` | 143 | **必需 464** (`model-viewer.js`) | |
75
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
 
package/SUPPORT.md CHANGED
@@ -97,9 +97,9 @@ grep 一条都数不到),还有 `skip "…"`(这台机器跑不了的门
97
97
 
98
98
  ### 试过一次的(🔍 人工实测,无回归保护)
99
99
 
100
- 7 行数据来自在真实设备上打开 [`capability-probe.html`](10-review/webview/capability-probe.html)。
100
+ 7 行数据来自在真实设备上打开 [`capability-probe.html`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/capability-probe.html)。
101
101
  完整 UA 与逐项结论在 [`docs/LAYER-BASELINE.md`](docs/LAYER-BASELINE.md);
102
- 15 项能力分别是什么、怎么自己复跑一遍,在 [`10-review/webview/RESULTS.md`](10-review/webview/RESULTS.md)。
102
+ 15 项能力分别是什么、怎么自己复跑一遍,在 [`10-review/webview/RESULTS.md`](https://github.com/owain323/frontend-lib/blob/master/10-review/webview/RESULTS.md)。
103
103
  本节 7 行**全部是 `🔍`** —— 没有一行有自动化守着。
104
104
 
105
105
  | 入口 | UA 自述的环境 | 实测 | 缺什么 |
@@ -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.5",
4
+ "libraryVersion": "0.26.0",
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 下的专属门禁 ⇒ 还没被机器验证过,不要用",
@@ -249,11 +249,13 @@
249
249
  },
250
250
  "aria": {
251
251
  "roles": [
252
- "img"
252
+ "img",
253
+ "region"
253
254
  ],
254
255
  "attrs": [
255
256
  "hidden",
256
- "label"
257
+ "label",
258
+ "labelledby"
257
259
  ]
258
260
  },
259
261
  "usesOtherNamespaces": [],
@@ -2411,7 +2413,9 @@
2411
2413
  "legacyClass": []
2412
2414
  },
2413
2415
  "aria": {
2414
- "roles": [],
2416
+ "roles": [
2417
+ "region"
2418
+ ],
2415
2419
  "attrs": [
2416
2420
  "hidden",
2417
2421
  "label"
@@ -2475,6 +2479,7 @@
2475
2479
  "aria": {
2476
2480
  "roles": [
2477
2481
  "alert",
2482
+ "region",
2478
2483
  "status"
2479
2484
  ],
2480
2485
  "attrs": [
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);