agentcss 0.14.0 → 0.15.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.
@@ -10,6 +10,7 @@
10
10
  | `Esc` | 关闭,并把焦点**还给按钮** |
11
11
  | `Tab` | 关闭菜单(焦点移到下一个元素)|
12
12
  | `Enter` / `Space` | 激活当前项 |
13
+ | 字母 / 数字(**可选**) | 跳到名字以该前缀开头的项(APG 标为 `(Optional)`;见下) |
13
14
 
14
15
  ## ⚠️ 另有两条容易漏的
15
16
 
@@ -66,12 +67,27 @@ menu.setAttribute('role', 'menu');
66
67
  `data-state` / `data-value`,**不用 class 切换** —— 这是全库 INVARIANT I-8
67
68
  (状态必须活在 DOM 属性上),也是门禁会查的东西。
68
69
 
69
- ## 成熟度:beta(两条行为债,如实列出)
70
+ ## 成熟度:stable(行为零欠账)
70
71
 
71
- | 债 | 现在的后果 |
72
+ 两条历史债都收编了,记账在 `ai/behaviors.json`:
73
+
74
+ | 曾经 | 现在 |
72
75
  |---|---|
73
- | `focus-return:manual` | 关闭后归位是手写的,不是统一机制 |
74
- | `typeahead:gap` | **没有类型搜索** —— APG 的 Menu Button 不硬性要求,但列表长时体验缺失 |
76
+ | `focus-return:manual` | 0.8.6 收编:`open()` 里 `save()`、`close()` 里 `restore()`,替掉手写的 `btn.focus()` |
77
+ | `typeahead:gap` | 0.15.0 收编:打字跳项走 typeahead 核(见下) |
78
+
79
+ ### 打字跳项(APG 的 `(Optional)` 项)
80
+
81
+ 打一个字母 ⇒ 跳到**下一个**以它开头的项;快速连打 ⇒ 前缀累积;连打同一个字母 ⇒
82
+ 在匹配项之间循环;不匹配 ⇒ **什么都不做**(APG 不要求"没找到"有反馈)。
83
+
84
+ ⚠️ **它天然只对拉丁字母标签有效**:中文标签靠输入法,而 IME 组字时 `keydown`
85
+ 给的是 `Process` / `Unidentified`,拿不到字符。所以判据用一张英文夹具测
86
+ (`dropdown-check` ⑦),demo 的中文菜单不受影响、也不会因此变慢。
87
+
88
+ ⚠️ 匹配前会**剥掉开头的装饰符号**:带勾选的项文本是「✓ 按日期」,
89
+ 不剥的话打「按」永远匹配不到。这和 table 列头把 `↕` 念出来是同一类问题 ——
90
+ 装饰符号不该混进"名字"里。
75
91
 
76
92
  ## 反例
77
93
 
@@ -223,6 +223,72 @@
223
223
  };
224
224
  /* ==== BEHAVIOR INJECT END: dismissable ==== */
225
225
 
226
+ /* ==== BEHAVIOR INJECT BEGIN: typeahead ==== */
227
+ var flTypeahead = function (opts) {
228
+ var opt = opts || {};
229
+ var items = opt.items || function () { return []; };
230
+ var textOf = opt.textOf || function (x) { return String((x && x.textContent) || ''); };
231
+ var usable = opt.usable || function () { return true; };
232
+ var timeout = opt.timeout || 500;
233
+ var onHit = opt.onHit || function () {};
234
+ var buf = '';
235
+ var timer = null;
236
+ var last = -1;
237
+
238
+ function stop() {
239
+ if (timer) { clearTimeout(timer); timer = null; }
240
+ }
241
+
242
+ /* 连打同一字符 ⇒ 只按首字符匹配(理由见文件头 ③④)*/
243
+ function repeated(s) {
244
+ if (s.length < 2) return false;
245
+ for (var i = 1; i < s.length; i++) {
246
+ if (s.charAt(i) !== s.charAt(0)) return false;
247
+ }
248
+ return true;
249
+ }
250
+
251
+ function find(needle) {
252
+ var l = items() || [];
253
+ var n = l.length;
254
+ if (!n) return -1;
255
+ for (var k = 0; k < n; k++) {
256
+ var i = (last + 1 + k) % n; /* ① 从当前项之后开始 ⇒ 会绕回 */
257
+ if (!usable(l[i])) continue; /* ⑤ */
258
+ if (textOf(l[i]).toLowerCase().indexOf(needle) === 0) return i;
259
+ }
260
+ return -1; /* ⑥ */
261
+ }
262
+
263
+ function type(ch) {
264
+ if (!ch) return -1;
265
+ buf += String(ch).toLowerCase(); /* ② */
266
+ stop();
267
+ timer = setTimeout(clear, timeout); /* ④ */
268
+ var needle = repeated(buf) ? buf.charAt(0) : buf; /* ③ */
269
+ var i = find(needle);
270
+ if (i >= 0) {
271
+ last = i;
272
+ onHit(i, (items() || [])[i], buf);
273
+ }
274
+ return i;
275
+ }
276
+
277
+ /* 只清缓冲,不清 last(理由见文件头)*/
278
+ function clear() {
279
+ buf = '';
280
+ stop();
281
+ }
282
+
283
+ return {
284
+ type: type,
285
+ clear: clear,
286
+ buffer: function () { return buf; },
287
+ destroy: function () { stop(); },
288
+ };
289
+ };
290
+ /* ==== BEHAVIOR INJECT END: typeahead ==== */
291
+
226
292
  /* ---- 旧环境兼容:Element.closest 在老 WebView 上不存在 ----
227
293
  直接调用会在真机上抛 `is not a function`(本地完全正常,
228
294
  只在老环境炸 —— 属静默失效)。这里做模块内兜底,
@@ -315,6 +381,8 @@
315
381
  焦点留在按钮上,菜单项永远进不去(真机用 NVDA + Edge 实测过)。
316
382
  APG 的菜单按钮也是这样:打开即把焦点交给第一项。 */
317
383
  focusAt(0);
384
+ /* 每次打开都从空缓冲开始:上次关菜单前打了一半的前缀不该留到这次 */
385
+ typeahead.clear();
318
386
  flEmit(root, 'fl-open', null);
319
387
  if (opt.onOpen) opt.onOpen();
320
388
  }
@@ -324,6 +392,7 @@
324
392
  menu.hidden = true;
325
393
  btn.setAttribute('aria-expanded', 'false');
326
394
  active = -1;
395
+ typeahead.clear();
327
396
  if (restoreFocus !== false) fr.restore();
328
397
  flEmit(root, 'fl-close', null);
329
398
  if (opt.onClose) opt.onClose();
@@ -359,6 +428,22 @@
359
428
  onMove: function (i) { focusAt(i); },
360
429
  });
361
430
 
431
+ /* ⑥ 打字跳项 —— APG Menu 的 `(Optional)` 项,走 typeahead 核。
432
+ 匹配**可见文本**,但先剥掉开头的装饰符号:带勾选的项文本是
433
+ "✓ 按日期",不剥的话打"按"永远匹配不到(同 table 列头 ↕ 那一类问题)。
434
+ ⚠️ 中文标签实际上打不出来:IME 走 keydown 时 key 是 Process / Unidentified。
435
+ 这条是给拉丁字母标签的菜单用的 —— APG 的 typeahead 本来就是这个前提,
436
+ 所以判据用一张英文夹具页测(见 dropdown-check ⑥)。 */
437
+ var typeahead = flTypeahead({
438
+ items: function () { return itemsOf(menu); },
439
+ textOf: function (el) {
440
+ var s = String((el && el.textContent) || '').replace(/\s+/g, ' ').trim();
441
+ return s.replace(/^[^0-9A-Za-z\u4e00-\u9fa5]+/, '');
442
+ },
443
+ usable: function (el) { return el.getAttribute('aria-disabled') !== 'true'; },
444
+ onHit: function (i) { focusAt(i); },
445
+ });
446
+
362
447
  /* ---------- 按钮 ---------- */
363
448
  /* 🔴 修:原来先 `setAttribute('aria-controls', menu.id || '')`
364
449
  **再**给 menu.id ⇒ aria-controls 永远拿到空串。
@@ -416,6 +501,17 @@
416
501
  留在原地会**关两次**(同一元素上的两个监听器,stopPropagation 挡不住
417
502
  同级的另一个监听器,那是 stopImmediatePropagation 的活)⇒ onClose 被调两遍。 */
418
503
 
504
+ /* ③b 可打印字符 ⇒ 跳到名字以它开头的项(APG 的 (Optional) 项)
505
+ · 空格排除:它的语义是"激活当前项"(下面 ④)
506
+ · 带 Ctrl / Meta / Alt 排除:那是浏览器快捷键
507
+ · isComposing 时排除:中文输入法正在组字,不能抢 */
508
+ if (k.length === 1 && k !== ' ' &&
509
+ !e.ctrlKey && !e.metaKey && !e.altKey && !e.isComposing) {
510
+ e.preventDefault();
511
+ typeahead.type(k);
512
+ return;
513
+ }
514
+
419
515
  /* ③ Tab 关闭(不拦截,让焦点自然走到下一个)*/
420
516
  if (k === 'Tab') { close(false); return; }
421
517
 
@@ -440,7 +536,10 @@
440
536
  return {
441
537
  open: open,
442
538
  close: close,
443
- destroy: function () { roving.destroy(); dismiss.destroy(); menu.hidden = true; },
539
+ destroy: function () {
540
+ roving.destroy(); dismiss.destroy(); typeahead.destroy();
541
+ menu.hidden = true;
542
+ },
444
543
  };
445
544
  }
446
545
 
package/CHANGELOG.md CHANGED
@@ -16,7 +16,57 @@
16
16
 
17
17
  (无)
18
18
 
19
- ## [0.14.0] - 2026-10-30
19
+ ## [0.15.0] - 2026-10-31
20
+
21
+ ### 行为 · dropdown 的「打字跳项」收编 typeahead 核(**最后一个 beta 升 stable**)
22
+
23
+ `ai/behaviors.json` 里最后一条真缺口是 `dropdown` 的 `typeahead:gap`:
24
+ APG Menu 把"按字符跳到名字以它开头的项"列为 `(Optional)`,本库确实没做。
25
+ 这一轮补上,走 `typeahead` 核(不新写一份)。
26
+
27
+ 接线时补了两件核不管的事:
28
+
29
+ - **打开/关闭都清缓冲**:上次关菜单前打了一半的前缀不该留到下次;
30
+ - **匹配前剥掉开头的装饰符号**:带勾选的项文本是「✓ 按日期」,
31
+ 不剥的话打「按」永远匹配不到。这和 table 列头把 `↕` 念出来是同一类问题 ——
32
+ 装饰符号混进了"名字"里。
33
+
34
+ ⚠️ 一个必须写明的限制:**中文标签打不出来**。IME 组字时 `keydown` 给的是
35
+ `Process` / `Unidentified`,拿不到字符。所以这条能力天然只对拉丁字母标签有效 ——
36
+ 这也是 APG 把它标成 Optional 的现实原因。判据因此用一张**英文夹具**测
37
+ (只换标签、跑的是同一份 `Dropdown.create`),demo 的中文菜单不受影响。
38
+
39
+ 判据 6 条,每条各有独立突变体(删分支 / 去掉同字母特判 / 不累积前缀 /
40
+ 不剥装饰符号 / 没找到就跳首项 —— 每次只有对应的那条红):
41
+
42
+ | 判据 | 断言 | 只被哪个突变体打红 |
43
+ |---|---|---|
44
+ | ⑦ 首字母 | 打 `p` ⇒ Paste | 删掉打字分支 |
45
+ | ⑦b 同字母循环 | `c` `c` ⇒ Copy → Cut | 去掉 `repeated()` 特判 |
46
+ | ⑦c 前缀累积 | `s` `e` ⇒ Select all | 把 `buf +=` 改成 `buf =` |
47
+ | ⑦d 不匹配不动 | 打 `z` 焦点不变 | `return -1` 改成 `return 0` |
48
+ | ⑦e 关闭态 | 关着打字不开菜单 | 删掉打字分支 |
49
+ | ⑦f 装饰符号 | 打 `c` 命中「✓ Copy」 | 去掉剥符号那一步 |
50
+
51
+ ### 判据 · 全库 stable 之后,ladder 怎么才算没瞎
52
+
53
+ 37 个组件全 stable,`maturity-gate` ③("不得全落同一档")按时报红。
54
+ 门禁自己写了两条路:加严判据,或显式声明台阶退役。这轮走了**第三条**,
55
+ 因为前两条都不诚实:为了"留几个 beta"而加严是拿数据凑判据,直接删掉 ③ 又是放走量具。
56
+
57
+ ⇒ ③ 改成:全库 stable 时先做**正向控制** —— 给一个真实组件临时塞一条合成债务,
58
+ 重算必须降成 beta;撤掉必须回到 stable。通过了才放行,并在输出里说明
59
+ "阶梯退而不休"。**分支里唯一的证据就是"判据仍然会红"**,不是一个允许全绿的开关。
60
+
61
+ 反向控制也跟着换了:原来的突变体是"把矩阵洗白"(全改成 core)——
62
+ 全库达标后这已经是**合法状态**,突变消失了。改成"**把判据改松**"
63
+ (让 `recompute` 忽略债务、一律返回 stable)⇒ 正向控制抓得到 ⇒ ③ 红,且只红这一条。
64
+
65
+ ### 体积
66
+
67
+ `dropdown.js` 多背一份 typeahead 核(gzip **6.4 → 7.6 KB / +18.9%**)。
68
+ 全库 gzip 258.5 → 259.7 KB(**+0.46%**)。基线按显式重录记下(`size-baseline.py --record`)。
69
+
20
70
 
21
71
  ### 行为 · drawer / overlay 的「关得掉」收编进核(beta → stable)
22
72
 
package/README.zh-CN.md CHANGED
@@ -83,7 +83,7 @@ npm install github:owain323/frontend-lib
83
83
 
84
84
  ## 组件一览
85
85
 
86
- 共 **37** 个(stable 36 / beta 1)。每个组件目录下有 `README.md`(为什么这么做)和 `demo.html`(能直接抄的结构)。
86
+ 共 **37** 个(stable 37 · 全部通过四条判据)。每个组件目录下有 `README.md`(为什么这么做)和 `demo.html`(能直接抄的结构)。
87
87
 
88
88
  ### primitives · 基础件(15)
89
89
 
package/START-HERE.md CHANGED
@@ -49,7 +49,7 @@
49
49
  | 日期区间 | `02-primitives/date-range/date-range.css` | 206 | **必需 301** (`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
- | 动作菜单(beta) | `03-patterns/dropdown/dropdown.css` | 135 | **必需 448** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
52
+ | 动作菜单 | `03-patterns/dropdown/dropdown.css` | 135 | **必需 547** (`dropdown.js`) | 要一组动作(新建 / 导出 / 删除)就要 |
53
53
  | 文字提示 | `03-patterns/tooltip/tooltip.css` | 89 | **必需 280** (`tooltip.js`) | 图标按钮或缩写需要一句话解释就要 |
54
54
  | 分页 | `03-patterns/pagination/pagination.css` | 195 | **必需 401** (`pagination.js`) | 数据多到要翻页就要 |
55
55
  | 树形 | `03-patterns/tree/tree.css` | 122 | **必需 464** (`tree.js`) | 层级数据(文件 / 组织 / 分类)要展开就要 |
package/ai/START-HERE.md CHANGED
@@ -30,7 +30,7 @@
30
30
  > 它的值直接从 `ai/components.json` 与目录实际内容读出来 —— **不信上面的说法,信这个**。
31
31
 
32
32
  <!-- ==== AI-INVENTORY-BEGIN ==== -->
33
- - 组件 **37** 个(`beta` 1 · `stable` 36)
33
+ - 组件 **37** 个(`stable` 37)
34
34
  - `04-recipes/`(页面级示例):`analysis-report`(组合型研究报告:正文 + 辅助栏并排,窄屏退回单栏) · `data-showcase`(四种数据表达方式对照:我到底该用哪个) · `detail-page`(实体详情页骨架:字段齐全 / 部分缺失 / 无数据,三态同构(DOM 不变)) · `longform`(单栏长文排版与阅读节奏) · `report`(报告版式系统 v1:共享基础 + 三种版式取舍(杂志 / 科研技术 / 金融研究)) · `table`(专业数据表格:列级格式与单位,三种用途预设)
35
35
  - `09-assets/`(图表与可视化):`bar`(柱状图,含图表规范的裁剪铁律) · `echarts-adapter`(把令牌喂给 ECharts,按需引入;本库不含 ECharts) · `model-viewer`(惰性加载的 3D 模型查看器) · `scientific-plot`(二维科学绘图:坐标轴 / 误差棒 / 置信区间带 / 对数轴) · `sparkline`(迷你趋势线,没有坐标轴的走势提示)
36
36
  <!-- ==== AI-INVENTORY-END ==== -->
package/ai/behaviors.json CHANGED
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "budget": {
31
31
  "manual": 0,
32
- "gap": 1
32
+ "gap": 0
33
33
  },
34
34
  "matrix": {
35
35
  "dismissable": {
@@ -316,8 +316,8 @@
316
316
  "没有候选项集合"
317
317
  ],
318
318
  "dropdown": [
319
- "gap",
320
- "APG 菜单支持'按首字母跳到菜单项'(可选增强),本库未实现;要做就走 typeahead 核"
319
+ "core",
320
+ "0.15.0 收编:APG Menu 的 `(Optional)` 项「按字符跳项」走 typeahead 核。删掉自写的匹配逻辑(本来也没有),接线时补了两件事:① 匹配前剥掉开头的装饰符号(带勾选的项文本是「✓ 按日期」,不剥就永远匹配不到,同 table 列头 ↕ 那类问题);② open/close 都清缓冲。判据 dropdown-check ⑦(首字母)/ ⑦b(同字母循环)/ ⑦c(前缀累积)/ ⑦d(不匹配不动)/ ⑦e(关着时打字不开菜单)/ ⑦f(装饰符号不进匹配文本),五条各有独立突变体。"
321
321
  ],
322
322
  "flip": [
323
323
  "n/a",
@@ -1,21 +1,20 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
3
  "generatedBy": "05-audit/gen-ai-contract.py",
4
- "libraryVersion": "0.14.0",
4
+ "libraryVersion": "0.15.0",
5
5
  "$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。",
6
6
  "maturityCriteria": {
7
7
  "alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
8
8
  "beta": "有 demo + 专属门禁,但**下面至少有一条没过**:① 状态仍用 .is-* class 表达(违反 INVARIANT I-8);② 该收编到微行为核的行为还是**手写副本**(manual)或**根本没做**(gap) ⇒ 改一处不会全库跟着变(I-14 的病)",
9
9
  "stable": "以上四条全过:有 demo + 专属门禁 + 状态全在 data-* 属性上 + 行为零欠账",
10
10
  "$note": "成熟度是【计算出来的】,不是拍的。手写把某个组件改成 stable 而它还有 manual / gap ⇒ 门禁会红。",
11
- "$whyFourth": "🔴 为什么第四条判据不能省:用前三条时多数组件会**全是 stable** ⇒ ladder 没有任何鉴别力(对标 Primer 六档阶梯,每档都有硬判据)。补上「行为欠账」这一条后:**stable 36 / beta 1**(本行由生成器算出,不要手改)。",
12
- "$strictImpact": "⚠️ strict 档位只放行 stable ⇒ 1 个组件在 strict 下不可用:dropdown。放宽 strict 会让「未收编的手写行为」进入生产路径。(本行由生成器算出,不要手改)"
11
+ "$whyFourth": "🔴 为什么第四条判据不能省:用前三条时多数组件会**全是 stable** ⇒ ladder 没有任何鉴别力(对标 Primer 六档阶梯,每档都有硬判据)。补上「行为欠账」这一条后:**stable 37 / beta 0**(本行由生成器算出,不要手改)。全库达标时 ladder 靠**正向控制**证明还有鉴别力(maturity-gate ③:塞一条合成债务必须降 beta),不是靠\"总得留几个 beta\"。",
12
+ "$strictImpact": "✅ strict 档位放行全部 37 个组件 —— 没有任何组件带着未收编的手写行为或未做的行为(本行由生成器算出,不要手改)"
13
13
  },
14
14
  "summary": {
15
15
  "total": 37,
16
16
  "byMaturity": {
17
- "stable": 36,
18
- "beta": 1
17
+ "stable": 37
19
18
  },
20
19
  "byTier": {
21
20
  "primitive": 15,
@@ -399,14 +398,12 @@
399
398
  "tier": "pattern",
400
399
  "purpose": "下拉菜单",
401
400
  "rootClass": "dd",
402
- "maturity": "beta",
401
+ "maturity": "stable",
403
402
  "maturityReasons": {
404
403
  "hasDemo": true,
405
404
  "hasDedicatedGate": true,
406
405
  "statesOnDomAttributes": true,
407
- "behaviorDebt": [
408
- "typeahead:gap"
409
- ]
406
+ "behaviorDebt": []
410
407
  },
411
408
  "gates": [
412
409
  "dropdown-check.js"
@@ -196,6 +196,68 @@ if (k >= 0) shared.stack.splice(k, 1);
196
196
  };
197
197
  };
198
198
 
199
+ var flTypeahead = function (opts) {
200
+ var opt = opts || {};
201
+ var items = opt.items || function () { return []; };
202
+ var textOf = opt.textOf || function (x) { return String((x && x.textContent) || ''); };
203
+ var usable = opt.usable || function () { return true; };
204
+ var timeout = opt.timeout || 500;
205
+ var onHit = opt.onHit || function () {};
206
+ var buf = '';
207
+ var timer = null;
208
+ var last = -1;
209
+
210
+ function stop() {
211
+ if (timer) { clearTimeout(timer); timer = null; }
212
+ }
213
+
214
+ function repeated(s) {
215
+ if (s.length < 2) return false;
216
+ for (var i = 1; i < s.length; i++) {
217
+ if (s.charAt(i) !== s.charAt(0)) return false;
218
+ }
219
+ return true;
220
+ }
221
+
222
+ function find(needle) {
223
+ var l = items() || [];
224
+ var n = l.length;
225
+ if (!n) return -1;
226
+ for (var k = 0; k < n; k++) {
227
+ var i = (last + 1 + k) % n;
228
+ if (!usable(l[i])) continue;
229
+ if (textOf(l[i]).toLowerCase().indexOf(needle) === 0) return i;
230
+ }
231
+ return -1;
232
+ }
233
+
234
+ function type(ch) {
235
+ if (!ch) return -1;
236
+ buf += String(ch).toLowerCase();
237
+ stop();
238
+ timer = setTimeout(clear, timeout);
239
+ var needle = repeated(buf) ? buf.charAt(0) : buf;
240
+ var i = find(needle);
241
+ if (i >= 0) {
242
+ last = i;
243
+ onHit(i, (items() || [])[i], buf);
244
+ }
245
+ return i;
246
+ }
247
+
248
+ function clear() {
249
+ buf = '';
250
+ stop();
251
+ }
252
+
253
+ return {
254
+ type: type,
255
+ clear: clear,
256
+ buffer: function () { return buf; },
257
+ destroy: function () { stop(); },
258
+ };
259
+ };
260
+
199
261
  function closest(el, sel) {
200
262
  if (!el) return null;
201
263
  if (el.closest) return el.closest(sel);
@@ -269,6 +331,8 @@ btn.setAttribute('aria-expanded', 'true');
269
331
  active = -1;
270
332
 
271
333
  focusAt(0);
334
+
335
+ typeahead.clear();
272
336
  flEmit(root, 'fl-open', null);
273
337
  if (opt.onOpen) opt.onOpen();
274
338
  }
@@ -278,6 +342,7 @@ function close(restoreFocus) {
278
342
  menu.hidden = true;
279
343
  btn.setAttribute('aria-expanded', 'false');
280
344
  active = -1;
345
+ typeahead.clear();
281
346
  if (restoreFocus !== false) fr.restore();
282
347
  flEmit(root, 'fl-close', null);
283
348
  if (opt.onClose) opt.onClose();
@@ -307,6 +372,16 @@ tabindex: false,
307
372
  onMove: function (i) { focusAt(i); },
308
373
  });
309
374
 
375
+ var typeahead = flTypeahead({
376
+ items: function () { return itemsOf(menu); },
377
+ textOf: function (el) {
378
+ var s = String((el && el.textContent) || '').replace(/\s+/g, ' ').trim();
379
+ return s.replace(/^[^0-9A-Za-z\u4e00-\u9fa5]+/, '');
380
+ },
381
+ usable: function (el) { return el.getAttribute('aria-disabled') !== 'true'; },
382
+ onHit: function (i) { focusAt(i); },
383
+ });
384
+
310
385
  if (!menu.id) menu.id = 'dd-menu-' + (new Date()).getTime();
311
386
  btn.setAttribute('aria-haspopup', 'true');
312
387
  btn.setAttribute('aria-expanded', 'false');
@@ -341,6 +416,13 @@ return;
341
416
 
342
417
  if (closest(e.target, '[data-dd-menu]') !== menu) return;
343
418
 
419
+ if (k.length === 1 && k !== ' ' &&
420
+ !e.ctrlKey && !e.metaKey && !e.altKey && !e.isComposing) {
421
+ e.preventDefault();
422
+ typeahead.type(k);
423
+ return;
424
+ }
425
+
344
426
  if (k === 'Tab') { close(false); return; }
345
427
 
346
428
  if (k === 'Enter' || k === ' ' || k === 'Spacebar') {
@@ -360,7 +442,10 @@ onDismiss: function (e, reason) { close(reason === 'esc'); },
360
442
  return {
361
443
  open: open,
362
444
  close: close,
363
- destroy: function () { roving.destroy(); dismiss.destroy(); menu.hidden = true; },
445
+ destroy: function () {
446
+ roving.destroy(); dismiss.destroy(); typeahead.destroy();
447
+ menu.hidden = true;
448
+ },
364
449
  };
365
450
  }
366
451
 
package/dist/COSTS.md CHANGED
@@ -36,7 +36,7 @@
36
36
  | pattern | `accordion` | 2.7 KB | 1.3 KB | 1.8 KB | **5.8 KB** | 34.1 KB | −28.3 KB |
37
37
  | pattern | `content` | 2.7 KB | 1.0 KB | — | **3.7 KB** | 24.2 KB | −20.5 KB |
38
38
  | pattern | `drawer` | 2.7 KB | 0.8 KB | 2.4 KB | **5.9 KB** | 26.1 KB | −20.2 KB |
39
- | pattern | `dropdown` | 2.7 KB | 0.8 KB | 2.9 KB | **6.4 KB** | 27.9 KB | −21.5 KB |
39
+ | pattern | `dropdown` | 2.7 KB | 0.8 KB | 3.4 KB | **6.9 KB** | 29.1 KB | −22.2 KB |
40
40
  | pattern | `form-validation` | 2.7 KB | 0.8 KB | — | **3.5 KB** | 24.1 KB | −20.7 KB |
41
41
  | pattern | `list` | 2.7 KB | 0.6 KB | — | **3.3 KB** | 22.0 KB | −18.7 KB |
42
42
  | pattern | `nav` | 2.7 KB | 1.2 KB | 2.6 KB | **6.6 KB** | 30.7 KB | −24.1 KB |
@@ -50,8 +50,8 @@
50
50
 
51
51
  ## 三、全库
52
52
 
53
- - 全部组件都用上(min,含 tokens 一份):**62.1 KB**
54
- - 源码版(带注释)全库 gzip:304.3 KB
53
+ - 全部组件都用上(min,含 tokens 一份):**62.6 KB**
54
+ - 源码版(带注释)全库 gzip:305.5 KB
55
55
 
56
56
  > ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 67% 是注释,字节口径)。
57
57
  > 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
@@ -82,10 +82,10 @@ python3 -m http.server 8000 --bind 127.0.0.1
82
82
 
83
83
  > **为什么后三行是补的**:`accordion` / `drawer` / `dropdown` 当时都是 beta 组件,
84
84
  > 而它们升 stable 的判据之一就是「行为零欠账」——人工读屏是这件事**唯一的用户侧证据**。
85
- > (0.14.0 把 drawer / overlay 的 dismissable 收编进核之后,两者已升 stable;
86
- > accordion 的 `roving:gap` 经核对 APG 原文后判定为**判据写错**(APG Accordion 没有
87
- > 方向键),已改为 `n/a`。`ai/components.json` 当前记 **1 个 beta**:dropdown
88
- > —— 它的 `typeahead:gap` 是真的:APG Menu 把"按字符跳项"列为 Optional,本库没做。)
85
+ > (0.14.0 把 drawer / overlay 的 dismissable 收编进核;accordion 的 `roving:gap`
86
+ > 经核对 APG 原文后判定为**判据写错**(APG Accordion 没有方向键),改为 `n/a`;
87
+ > 0.15.0 补上 dropdown 的 typeahead。`ai/components.json` 当前 **37 个全 stable**,
88
+ > 阶梯靠正向控制证明仍能区分有债的组件。)
89
89
  > 只覆盖 `overlay` 一个浮层,等于对其他浮层的真实表现没有任何记录。
90
90
 
91
91
  **本轮跑出来并已修掉的一条真缺陷**:`table` 的列头按钮被念成「公司 · `↕` · 按钮」——
@@ -5,7 +5,7 @@
5
5
 
6
6
  | 项目 | 值 |
7
7
  |---|---|
8
- | 声明适用版本 | agentcss `0.14.0` |
8
+ | 声明适用版本 | agentcss `0.15.0` |
9
9
  | 声明日期 | 2026-10-30 |
10
10
  | 复核节奏 | 每次发版前重跑(`npm run gate` 内含全部证据) |
11
11
  | 范围 | 本库交付的 CSS / JS、全部 `demo.html`(组件页 + 图表页 + 组合页)、以及文档站生成的页面 |
package/index.html CHANGED
@@ -94,7 +94,7 @@ li a:active { background: var(--surface-sunken); }
94
94
  </p>
95
95
  </main>
96
96
  <footer class="wrap">
97
- <p class="note">0.14.0 · 零第三方运行时依赖</p>
97
+ <p class="note">0.15.0 · 零第三方运行时依赖</p>
98
98
  </footer>
99
99
  </body>
100
100
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcss",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "移动 Web 组件库:CSS + 可选行为脚本,零第三方运行时依赖。样式用 bundler 引入,交互行为用 script 标签引入。",
5
5
  "keywords": [
6
6
  "css",