agentcss 0.0.0-stage → 0.11.2

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 (274) hide show
  1. package/01-tokens/README.md +236 -0
  2. package/01-tokens/behavior/dismissable.js +132 -0
  3. package/01-tokens/behavior/emit.js +76 -0
  4. package/01-tokens/behavior/focus-return.js +81 -0
  5. package/01-tokens/behavior/roving.js +152 -0
  6. package/01-tokens/behavior/typeahead.js +124 -0
  7. package/01-tokens/focus-ring-demo.html +133 -0
  8. package/01-tokens/focus-ring.css +102 -0
  9. package/01-tokens/page.css +244 -0
  10. package/01-tokens/polyfill.js +116 -0
  11. package/01-tokens/responsive-demo.html +171 -0
  12. package/01-tokens/scrollbar-demo.html +194 -0
  13. package/01-tokens/scrollbar.css +179 -0
  14. package/01-tokens/theme-toggle.js +103 -0
  15. package/01-tokens/tokens.css +972 -0
  16. package/01-tokens/typography.css +156 -0
  17. package/02-primitives/badge/README.md +71 -0
  18. package/02-primitives/badge/badge.css +141 -0
  19. package/02-primitives/badge/demo.html +101 -0
  20. package/02-primitives/button/README.md +99 -0
  21. package/02-primitives/button/button.css +353 -0
  22. package/02-primitives/button/demo.html +200 -0
  23. package/02-primitives/card/README.md +145 -0
  24. package/02-primitives/card/card.css +246 -0
  25. package/02-primitives/card/demo.html +170 -0
  26. package/02-primitives/choice/README.md +161 -0
  27. package/02-primitives/choice/choice.css +395 -0
  28. package/02-primitives/choice/demo.html +277 -0
  29. package/02-primitives/combobox/README.md +79 -0
  30. package/02-primitives/combobox/combobox.css +223 -0
  31. package/02-primitives/combobox/combobox.js +485 -0
  32. package/02-primitives/combobox/demo.html +109 -0
  33. package/02-primitives/date-range/README.md +100 -0
  34. package/02-primitives/date-range/date-range.css +206 -0
  35. package/02-primitives/date-range/date-range.js +301 -0
  36. package/02-primitives/date-range/demo.html +104 -0
  37. package/02-primitives/input/README.md +170 -0
  38. package/02-primitives/input/demo.html +246 -0
  39. package/02-primitives/input/input.css +239 -0
  40. package/02-primitives/popover/README.md +80 -0
  41. package/02-primitives/popover/demo.html +150 -0
  42. package/02-primitives/popover/popover.css +187 -0
  43. package/02-primitives/popover/popover.js +331 -0
  44. package/02-primitives/progress/README.md +82 -0
  45. package/02-primitives/progress/demo.html +154 -0
  46. package/02-primitives/progress/progress.css +172 -0
  47. package/02-primitives/select/README.md +95 -0
  48. package/02-primitives/select/demo.html +121 -0
  49. package/02-primitives/select/select.css +151 -0
  50. package/02-primitives/select/select.js +549 -0
  51. package/02-primitives/separator/README.md +63 -0
  52. package/02-primitives/separator/demo.html +84 -0
  53. package/02-primitives/separator/separator.css +73 -0
  54. package/02-primitives/skeleton/README.md +69 -0
  55. package/02-primitives/skeleton/demo.html +110 -0
  56. package/02-primitives/skeleton/skeleton.css +96 -0
  57. package/02-primitives/switch/README.md +65 -0
  58. package/02-primitives/switch/demo.html +115 -0
  59. package/02-primitives/switch/switch.css +370 -0
  60. package/03-patterns/accordion/README.md +121 -0
  61. package/03-patterns/accordion/accordion.css +453 -0
  62. package/03-patterns/accordion/accordion.js +369 -0
  63. package/03-patterns/accordion/demo.html +223 -0
  64. package/03-patterns/content/README.md +103 -0
  65. package/03-patterns/content/content.css +286 -0
  66. package/03-patterns/content/demo.html +88 -0
  67. package/03-patterns/drawer/README.md +79 -0
  68. package/03-patterns/drawer/demo.html +101 -0
  69. package/03-patterns/drawer/drawer.css +129 -0
  70. package/03-patterns/drawer/drawer.js +258 -0
  71. package/03-patterns/dropdown/README.md +78 -0
  72. package/03-patterns/dropdown/demo.html +101 -0
  73. package/03-patterns/dropdown/dropdown.css +135 -0
  74. package/03-patterns/dropdown/dropdown.js +451 -0
  75. package/03-patterns/form-validation/README.md +147 -0
  76. package/03-patterns/form-validation/demo.html +280 -0
  77. package/03-patterns/form-validation/form-validation.css +263 -0
  78. package/03-patterns/list/README.md +150 -0
  79. package/03-patterns/list/demo.html +225 -0
  80. package/03-patterns/list/flip.js +201 -0
  81. package/03-patterns/list/list.css +162 -0
  82. package/03-patterns/nav/README.md +225 -0
  83. package/03-patterns/nav/demo.html +131 -0
  84. package/03-patterns/nav/nav.css +314 -0
  85. package/03-patterns/nav/nav.js +430 -0
  86. package/03-patterns/nav/toc.js +111 -0
  87. package/03-patterns/overlay/README.md +196 -0
  88. package/03-patterns/overlay/demo.html +241 -0
  89. package/03-patterns/overlay/overlay.css +319 -0
  90. package/03-patterns/overlay/overlay.js +563 -0
  91. package/03-patterns/pagination/README.md +114 -0
  92. package/03-patterns/pagination/demo.html +165 -0
  93. package/03-patterns/pagination/pagination.css +195 -0
  94. package/03-patterns/pagination/pagination.js +395 -0
  95. package/03-patterns/reveal/README.md +91 -0
  96. package/03-patterns/reveal/demo.html +106 -0
  97. package/03-patterns/reveal/reveal.css +93 -0
  98. package/03-patterns/reveal/reveal.js +172 -0
  99. package/03-patterns/states/README.md +141 -0
  100. package/03-patterns/states/demo.html +240 -0
  101. package/03-patterns/states/states.css +252 -0
  102. package/03-patterns/tabs/README.md +84 -0
  103. package/03-patterns/tabs/demo.html +139 -0
  104. package/03-patterns/tabs/tabs.css +172 -0
  105. package/03-patterns/tabs/tabs.js +307 -0
  106. package/03-patterns/tooltip/README.md +96 -0
  107. package/03-patterns/tooltip/demo.html +78 -0
  108. package/03-patterns/tooltip/tooltip.css +89 -0
  109. package/03-patterns/tooltip/tooltip.js +288 -0
  110. package/03-patterns/tree/README.md +94 -0
  111. package/03-patterns/tree/demo.html +138 -0
  112. package/03-patterns/tree/tree.css +113 -0
  113. package/03-patterns/tree/tree.js +456 -0
  114. package/04-recipes/analysis-report/README.md +80 -0
  115. package/04-recipes/analysis-report/analysis-report.css +187 -0
  116. package/04-recipes/analysis-report/demo.html +274 -0
  117. package/04-recipes/data-showcase/README.md +45 -0
  118. package/04-recipes/data-showcase/demo.html +247 -0
  119. package/04-recipes/detail-page/README.md +93 -0
  120. package/04-recipes/detail-page/demo.html +55 -0
  121. package/04-recipes/detail-page/detail-page.css +190 -0
  122. package/04-recipes/detail-page/full.html +60 -0
  123. package/04-recipes/detail-page/null.html +60 -0
  124. package/04-recipes/detail-page/partial.html +60 -0
  125. package/04-recipes/detail-page/verify-skeleton.py +19 -0
  126. package/04-recipes/longform/README.md +93 -0
  127. package/04-recipes/longform/agc-memory.svg +4 -0
  128. package/04-recipes/longform/content.html +610 -0
  129. package/04-recipes/longform/content.md +51 -0
  130. package/04-recipes/longform/longform.html +627 -0
  131. package/04-recipes/report/README.md +176 -0
  132. package/04-recipes/report/demo.html +355 -0
  133. package/04-recipes/report/report.css +364 -0
  134. package/04-recipes/table/README.md +103 -0
  135. package/04-recipes/table/demo.html +455 -0
  136. package/04-recipes/table/table.css +372 -0
  137. package/04-recipes/tierA-tokens.css +189 -0
  138. package/09-assets/README.md +74 -0
  139. package/09-assets/bar/README.md +94 -0
  140. package/09-assets/bar/bar.css +166 -0
  141. package/09-assets/bar/bar.js +320 -0
  142. package/09-assets/bar/demo.html +176 -0
  143. package/09-assets/echarts-adapter/demo.html +192 -0
  144. package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
  145. package/09-assets/model-viewer/README.md +155 -0
  146. package/09-assets/model-viewer/demo.html +105 -0
  147. package/09-assets/model-viewer/model-viewer.css +143 -0
  148. package/09-assets/model-viewer/model-viewer.js +423 -0
  149. package/09-assets/scientific-plot/README.md +94 -0
  150. package/09-assets/scientific-plot/demo.html +252 -0
  151. package/09-assets/scientific-plot/scientific-plot.css +141 -0
  152. package/09-assets/scientific-plot/scientific-plot.js +610 -0
  153. package/09-assets/sparkline/README.md +153 -0
  154. package/09-assets/sparkline/demo.html +163 -0
  155. package/09-assets/sparkline/sparkline.css +158 -0
  156. package/09-assets/sparkline/sparkline.js +508 -0
  157. package/API.md +332 -0
  158. package/CHANGELOG.md +3116 -0
  159. package/CODE_OF_CONDUCT.md +64 -0
  160. package/CONTRIBUTING.md +89 -0
  161. package/ERRORS.md +1658 -0
  162. package/LICENSE +21 -0
  163. package/README.md +491 -2
  164. package/SECURITY.md +72 -0
  165. package/START-HERE.md +164 -0
  166. package/SUPPORT.md +109 -0
  167. package/adapters/presentation/README.md +113 -0
  168. package/adapters/presentation/check.js +101 -0
  169. package/adapters/presentation/deck.schema.json +74 -0
  170. package/adapters/presentation/to-doc.js +138 -0
  171. package/ai/START-HERE.md +104 -0
  172. package/ai/behaviors.json +380 -0
  173. package/ai/behaviors.schema.json +61 -0
  174. package/ai/capabilities.json +56 -0
  175. package/ai/cli.js +387 -0
  176. package/ai/components.json +1944 -0
  177. package/ai/components.meta.json +168 -0
  178. package/ai/components.schema.json +220 -0
  179. package/ai/contract.schema.json +147 -0
  180. package/ai/deprecations.json +55 -0
  181. package/ai/deprecations.schema.json +85 -0
  182. package/ai/motion-policy.json +22 -0
  183. package/ai/patch.js +323 -0
  184. package/ai/patch.schema.json +63 -0
  185. package/ai/profiles.json +92 -0
  186. package/ai/tokens.audience.json +55 -0
  187. package/ai/tokens.audience.schema.json +73 -0
  188. package/ai/tokens.json +762 -0
  189. package/ai/tokens.schema.json +58 -0
  190. package/ai/tokens.tree.json +752 -0
  191. package/ai/tokens.tree.schema.json +56 -0
  192. package/ai/validate.js +383 -0
  193. package/dist/01-tokens/behavior/dismissable.js +60 -0
  194. package/dist/01-tokens/behavior/emit.js +21 -0
  195. package/dist/01-tokens/behavior/focus-return.js +31 -0
  196. package/dist/01-tokens/behavior/roving.js +77 -0
  197. package/dist/01-tokens/behavior/typeahead.js +61 -0
  198. package/dist/01-tokens/focus-ring.css +1 -0
  199. package/dist/01-tokens/page.css +1 -0
  200. package/dist/01-tokens/polyfill.js +78 -0
  201. package/dist/01-tokens/scrollbar.css +1 -0
  202. package/dist/01-tokens/theme-toggle.js +61 -0
  203. package/dist/01-tokens/tokens.css +1 -0
  204. package/dist/01-tokens/typography.css +1 -0
  205. package/dist/02-primitives/badge/badge.css +1 -0
  206. package/dist/02-primitives/button/button.css +1 -0
  207. package/dist/02-primitives/card/card.css +1 -0
  208. package/dist/02-primitives/choice/choice.css +1 -0
  209. package/dist/02-primitives/combobox/combobox.css +1 -0
  210. package/dist/02-primitives/combobox/combobox.js +439 -0
  211. package/dist/02-primitives/date-range/date-range.css +1 -0
  212. package/dist/02-primitives/date-range/date-range.js +267 -0
  213. package/dist/02-primitives/input/input.css +1 -0
  214. package/dist/02-primitives/popover/popover.css +1 -0
  215. package/dist/02-primitives/popover/popover.js +257 -0
  216. package/dist/02-primitives/progress/progress.css +1 -0
  217. package/dist/02-primitives/select/select.css +1 -0
  218. package/dist/02-primitives/select/select.js +449 -0
  219. package/dist/02-primitives/separator/separator.css +1 -0
  220. package/dist/02-primitives/skeleton/skeleton.css +1 -0
  221. package/dist/02-primitives/switch/switch.css +1 -0
  222. package/dist/03-patterns/accordion/accordion.css +1 -0
  223. package/dist/03-patterns/accordion/accordion.js +219 -0
  224. package/dist/03-patterns/content/content.css +1 -0
  225. package/dist/03-patterns/drawer/drawer.css +1 -0
  226. package/dist/03-patterns/drawer/drawer.js +229 -0
  227. package/dist/03-patterns/dropdown/dropdown.css +1 -0
  228. package/dist/03-patterns/dropdown/dropdown.js +365 -0
  229. package/dist/03-patterns/form-validation/form-validation.css +1 -0
  230. package/dist/03-patterns/list/flip.js +117 -0
  231. package/dist/03-patterns/list/list.css +1 -0
  232. package/dist/03-patterns/nav/nav.css +1 -0
  233. package/dist/03-patterns/nav/nav.js +322 -0
  234. package/dist/03-patterns/nav/toc.js +76 -0
  235. package/dist/03-patterns/overlay/overlay.css +1 -0
  236. package/dist/03-patterns/overlay/overlay.js +398 -0
  237. package/dist/03-patterns/pagination/pagination.css +1 -0
  238. package/dist/03-patterns/pagination/pagination.js +273 -0
  239. package/dist/03-patterns/reveal/reveal.css +1 -0
  240. package/dist/03-patterns/reveal/reveal.js +138 -0
  241. package/dist/03-patterns/states/states.css +1 -0
  242. package/dist/03-patterns/tabs/tabs.css +1 -0
  243. package/dist/03-patterns/tabs/tabs.js +242 -0
  244. package/dist/03-patterns/tooltip/tooltip.css +1 -0
  245. package/dist/03-patterns/tooltip/tooltip.js +213 -0
  246. package/dist/03-patterns/tree/tree.css +1 -0
  247. package/dist/03-patterns/tree/tree.js +383 -0
  248. package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
  249. package/dist/04-recipes/detail-page/detail-page.css +1 -0
  250. package/dist/04-recipes/report/report.css +1 -0
  251. package/dist/04-recipes/table/table.css +1 -0
  252. package/dist/04-recipes/tierA-tokens.css +1 -0
  253. package/dist/09-assets/bar/bar.css +1 -0
  254. package/dist/09-assets/bar/bar.js +222 -0
  255. package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
  256. package/dist/09-assets/model-viewer/model-viewer.css +1 -0
  257. package/dist/09-assets/model-viewer/model-viewer.js +320 -0
  258. package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
  259. package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
  260. package/dist/09-assets/sparkline/sparkline.css +1 -0
  261. package/dist/09-assets/sparkline/sparkline.js +265 -0
  262. package/dist/COSTS.md +56 -0
  263. package/docs/AI-CONTRACT.md +197 -0
  264. package/docs/BACKLOG.md +127 -0
  265. package/docs/BENCHMARK.md +252 -0
  266. package/docs/GUIDANCE.md +154 -0
  267. package/docs/INVARIANT.md +372 -0
  268. package/docs/LAYER-BASELINE.md +158 -0
  269. package/docs/RELEASING.md +275 -0
  270. package/docs/RFC-THEME-SCOPE.md +141 -0
  271. package/docs/ROADMAP.md +182 -0
  272. package/index.html +82 -0
  273. package/package.json +107 -4
  274. package/types/index.d.ts +310 -0
@@ -0,0 +1,100 @@
1
+ # date-range · 日期区间
2
+
3
+ 财务/成本场景刚需:**"看上季度"、"对比去年同期"**。
4
+
5
+ ## 🔴 三件容易做错的事(写在最前面)
6
+
7
+ | # | 做什么 | 不做会怎样 |
8
+ |---|---|---|
9
+ | ① | **必须有「清除」按钮** | 日期填错了,用户只能一个个退格删 |
10
+ | ② | **`min`/`max` 越界与"开始晚于结束"要明确报错** | 用户不知道自己填错了,还以为没生效 |
11
+ | ③ | 快捷按钮必须能 `Tab` 到 | 键盘用户完全用不了"看上季度" |
12
+
13
+ ## 结构
14
+
15
+ ```html
16
+ <div class="drange">
17
+ <label class="drange__label">
18
+ <span>开始</span>
19
+ <input class="drange__input" type="date" data-dr-from>
20
+ </label>
21
+ <span class="drange__sep" aria-hidden="true">→</span>
22
+ <label class="drange__label">
23
+ <span>结束</span>
24
+ <input class="drange__input" type="date" data-dr-to>
25
+ </label>
26
+ <button class="drange__clear" type="button" data-dr-clear>清除</button>
27
+ <div class="drange__presets">…上季度 / 上月 / 今年…</div>
28
+ <p data-dr-error role="alert"></p>
29
+ <p data-dr-summary></p>
30
+ </div>
31
+ ```
32
+
33
+ | 部件 | 说明 |
34
+ |---|---|
35
+ | `.drange` | 根,`flex-wrap` —— 窄屏自动换行 |
36
+ | `[data-dr-from]` / `[data-dr-to]` | 两个日期输入,**缺任一个直接抛错** |
37
+ | `[data-dr-error]` | 错误容器,`role="alert"` |
38
+ | `[data-dr-summary]` | 选中区间的摘要(如「共 92 天」)|
39
+ | `[data-dr-clear]` | 清除,**不可省** |
40
+ | `.drange--disabled` | 整体禁用 |
41
+
42
+ ⚠️ 标记名是 **`data-dr-*`**(`dr` 缩写),不是 `data-drange-*`。
43
+
44
+ ## 启用
45
+
46
+ ```js
47
+ const dr = DateRange.create(元素, {
48
+ presets: [{ label: '上季度', from: …, to: … }],
49
+ min: '2020-01-01', max: '2026-12-31',
50
+ });
51
+ ```
52
+
53
+ 同时导出的一批**纯函数**(可单独用,很 handy):
54
+
55
+ ```js
56
+ DateRange.iso(date) // → 'YYYY-MM-DD'
57
+ DateRange.addDays(d, n)
58
+ DateRange.addMonths(d, n)
59
+ DateRange.quarterOf(d) // → 0..3(0 = Q1)
60
+ DateRange.quarterRange(y, q) // 季度必须是 0-3,传别的直接抛错
61
+ ```
62
+
63
+ ⚠️ `quarterOf` 曾经是**内部函数但忘了导出** —— 使用者拿到了 `quarterRange`
64
+ 却无法从任意日期反推季度号。是单测抓出来的,补导出(纯函数,无副作用)。
65
+
66
+ ## 校验做了什么
67
+
68
+ | 情况 | 行为 |
69
+ |---|---|
70
+ | 开始 > 结束 | 报「开始日期不能晚于结束日期(当前:…)」|
71
+ | 早于 `min` | 报「开始日期不能早于 …」|
72
+ | 晚于 `max` | 报「结束日期不能晚于 …」|
73
+ | `quarterRange` 季度号越界 | 抛错并说明「季度必须是 0-3(0=Q1),收到 …」|
74
+
75
+ ### 🔴 错误要同时落到字段上,不只是显示一句话
76
+
77
+ ```js
78
+ from.setAttribute('aria-invalid', msg ? 'true' : 'false');
79
+ to.setAttribute('aria-invalid', msg ? 'true' : 'false');
80
+ if (msg) {
81
+ errBox.id = 'dr-err-N';
82
+ from.setAttribute('aria-describedby', id); // to 同理
83
+ }
84
+ ```
85
+
86
+ **为什么**:只显示一行红字,读屏用户**不知道是哪个字段错了**。
87
+ `aria-invalid` 标记问题字段,`aria-describedby` 把错误挂到字段上,
88
+ 三件一起做才完整。
89
+
90
+ `opt.min` / `opt.max` 会**同时**写到两个输入的原生 `min`/`max` 属性上
91
+ (浏览器自带的日期选择器就会拦住越界值,不只是我们事后校验)。
92
+
93
+ ## 成熟度:stable(无行为债)
94
+
95
+ 契约里 `behaviorDebt` 为空 —— 清除、校验、preset 接线都有门禁覆盖
96
+ (`05-audit/date-range-check.js`)。
97
+
98
+ ## 门禁
99
+
100
+ `05-audit/date-range-check.js`
@@ -0,0 +1,206 @@
1
+ /* ============================================================================
2
+ date-range.css — 日期区间(E3 /
3
+ ============================================================================
4
+ 财务/成本场景刚需("看上季度" "对比去年同期")。
5
+
6
+ ⭐ 三件容易做错的事:
7
+ ① **必须有「清除」**:日期填错了要能一键清空
8
+ (没有清除按钮 = 用户只能一个个退格删日期)
9
+ ② **min/max 约束**:开始晚于结束、或超出范围时要**明确报错**
10
+ ③ **键盘可达**:三个快捷按钮(上季度/上月/今年)必须能 Tab 到
11
+ ============================================================================ */
12
+
13
+ .drange {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ align-items: flex-end;
17
+ gap: var(--sp-3);
18
+ }
19
+
20
+ .drange__field { flex: 1 1 10rem; min-width: 0; }
21
+
22
+ .drange__label {
23
+ display: block;
24
+ margin-bottom: var(--sp-1);
25
+ font-size: var(--fs-xs);
26
+ color: var(--text-secondary);
27
+ }
28
+
29
+ .drange__input {
30
+ display: block;
31
+ width: 100%;
32
+ /* 🔴 必须:input 的 UA 盒模型是 content-box,width:100% 只是内容宽,
33
+ 加 padding+border 实际比容器宽 26px ⇒ 393px 视口横向溢出。 */
34
+ box-sizing: border-box;
35
+ min-height: 44px; /* 🔴 命中区达标 */
36
+ padding: 0 var(--sp-3);
37
+ background: var(--surface);
38
+ color: var(--text-primary);
39
+ border: 1px solid var(--border-control);
40
+ border-radius: var(--r-sm);
41
+ font: inherit;
42
+ font-size: var(--fs-sm);
43
+ font-family: var(--mono);
44
+ font-variant-numeric: tabular-nums;
45
+ color-scheme: light dark; /* ⭐ 让原生日期选择器跟随主题 */
46
+ }
47
+
48
+ .drange__input:focus-visible {
49
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
50
+ outline-offset: var(--focus-ring-offset);
51
+ }
52
+
53
+ /* ⭐ 错误态:边框 + 图标(**不能只改颜色**)*/
54
+ .drange__input[aria-invalid="true"] {
55
+ border-color: var(--danger);
56
+ border-width: 2px;
57
+ background: var(--surface-sunken);
58
+ }
59
+
60
+ .drange__input[aria-invalid="true"]::after { content: " ⚠"; }
61
+
62
+ .drange__sep {
63
+ flex: 0 0 auto;
64
+ padding-bottom: var(--sp-3);
65
+ color: var(--text-tertiary);
66
+ font-size: var(--fs-sm);
67
+ }
68
+
69
+ /* ---------- 快捷按钮 ---------- */
70
+ .drange__presets {
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ gap: var(--sp-2);
74
+ margin-top: var(--sp-3);
75
+ }
76
+
77
+ .drange__preset {
78
+ min-height: 32px; /* 密集数据表惯例(非 44)*/
79
+ padding: 0 var(--sp-3);
80
+ background: var(--surface);
81
+ color: var(--text-secondary);
82
+ border: 1px solid var(--border-control);
83
+ border-radius: var(--r-pill);
84
+ font: inherit;
85
+ font-size: var(--fs-xs);
86
+ cursor: pointer;
87
+ }
88
+
89
+ .drange__preset:focus-visible {
90
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
91
+ outline-offset: var(--focus-ring-offset);
92
+ }
93
+
94
+ @media (hover: hover) and (pointer: fine) {
95
+ .drange__preset:hover { background: var(--surface-sunken); }
96
+ }
97
+
98
+ .drange__preset[aria-pressed="true"] {
99
+ background: var(--accent-soft);
100
+ border-color: var(--accent);
101
+ color: var(--text-primary);
102
+ font-weight: 600;
103
+ }
104
+
105
+ /* ---------- 错误提示 ---------- */
106
+ /* 🔴 为什么这里保留彩色竖条(0.8.4 · 全库竖条政策)
107
+ 本库整体立场是去掉纯装饰竖条(理由见 typography.css / form-validation.css)。
108
+ 但**错误提示**是例外:竖条在这里承担的是**状态语义** ——
109
+ "这个字段错了",与红色底色、`aria-invalid` 是同一份信息的三个出口
110
+ (视觉 / 颜色 / 无障碍)。
111
+ ⚠️ 和 form-validation 的区别:那里原本也是靠竖条提示错误,
112
+ 被判为「理由对、手段错」⇒ 改用**图标**;这里保留是因为
113
+ 它同时已有 `role="alert"` + `aria-invalid`,竖条只是**第三重冗余**,
114
+ 拿掉之后单色 / 色弱用户仍可靠图标与 aria 察觉,
115
+ ⇒ 不构成"唯一信息出口"。 */
116
+ .drange__error {
117
+ flex: 1 1 100%;
118
+ display: flex;
119
+ align-items: flex-start;
120
+ gap: var(--sp-2);
121
+ padding: var(--sp-2) var(--sp-3);
122
+ background: var(--surface-sunken);
123
+ border-inline-start: 3px solid var(--danger);
124
+ border-radius: var(--r-sm);
125
+ color: var(--danger);
126
+ font-size: var(--fs-xs);
127
+ line-height: 1.5;
128
+ }
129
+
130
+ .drange__error[hidden] { display: none; }
131
+
132
+ /* ---------- 摘要 ---------- */
133
+ .drange__summary {
134
+ flex: 1 1 100%;
135
+ margin-top: var(--sp-2);
136
+ color: var(--text-secondary);
137
+ font-size: var(--fs-xs);
138
+ }
139
+
140
+ .drange__summary b {
141
+ font-family: var(--mono);
142
+ font-variant-numeric: tabular-nums;
143
+ color: var(--text-primary);
144
+ }
145
+
146
+ /* ---------- 清除按钮:44px 命中区 ---------- */
147
+ .drange__clear {
148
+ flex: 0 0 auto;
149
+ min-height: 44px;
150
+ padding: 0 var(--sp-4);
151
+ background: var(--surface);
152
+ color: var(--text-secondary);
153
+ border: 1px solid var(--border-control);
154
+ border-radius: var(--r-sm);
155
+ font: inherit;
156
+ font-size: var(--fs-sm);
157
+ cursor: pointer;
158
+ }
159
+
160
+ .drange__clear:focus-visible {
161
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
162
+ outline-offset: var(--focus-ring-offset);
163
+ }
164
+
165
+ @media (hover: hover) and (pointer: fine) {
166
+ .drange__clear:hover { background: var(--surface-sunken); color: var(--text-primary); }
167
+ }
168
+
169
+ .drange__clear[disabled] {
170
+ background: var(--surface-sunken);
171
+ color: var(--text-tertiary);
172
+ cursor: not-allowed;
173
+ }
174
+
175
+ @media (prefers-reduced-motion: reduce) {
176
+ .drange * { transition: none; }
177
+ }
178
+
179
+ /* ---------- 禁用态(states 门禁要求:HIGH 级形态不能缺)---------- */
180
+ .drange__input:disabled {
181
+ background: var(--surface-sunken);
182
+ border-color: var(--border-decor);
183
+ color: var(--text-tertiary);
184
+ cursor: not-allowed;
185
+ /* ⛔ 不写 opacity —— 那会把文字对比度一起拉低 */
186
+ }
187
+
188
+ .drange__input:disabled:focus-visible {
189
+ outline: none; /* 禁用态不该有焦点环 */
190
+ }
191
+
192
+ .drange--disabled .drange__preset,
193
+ .drange--disabled .drange__clear {
194
+ background: var(--surface-sunken);
195
+ color: var(--text-tertiary);
196
+ border-color: var(--border-decor);
197
+ cursor: not-allowed;
198
+ }
199
+
200
+ @media (hover: hover) and (pointer: fine) {
201
+ .drange--disabled .drange__preset:hover,
202
+ .drange--disabled .drange__clear:hover {
203
+ background: var(--surface-sunken);
204
+ color: var(--text-tertiary);
205
+ }
206
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * date-range.js — 日期区间(E3 /
3
+ *
4
+ * 财务/成本场景刚需:看上季度、对比去年同期。
5
+ *
6
+ * ⭐ 三个关键点(最容易漏):
7
+ * ① **必须有「清除」**:日期填错了要能一键清空
8
+ * ⇒ 没有它 = 用户只能一个个退格删
9
+ * ② **约束校验**:开始晚于结束、超出 min/max ⇒ **明确报错**(不静默)
10
+ * ③ **无障碍**:每个 input 有 label;错误用 role=alert 播报
11
+ *
12
+ * ⭐ 为什么**基于原生 `<input type="date">`** 而不是自建日历:
13
+ * 原生自带:触屏日历面板、本地化、键盘输入、无障碍语义。
14
+ * 自建日历要重做这一切,且在移动端体验更差。
15
+ * (与 combobox 的取舍相反 —— 那里原生 select 无法定制样式。)
16
+ *
17
+ * 无依赖 · ES5
18
+ */
19
+ (function (global) {
20
+ 'use strict';
21
+ /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
22
+ var flEmit = function (el, name, detail) {
23
+ if (!el || !el.dispatchEvent) return null;
24
+ var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
25
+ var ev = null;
26
+ /* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
27
+ if (typeof window.CustomEvent === 'function') {
28
+ try {
29
+ ev = new window.CustomEvent(type, {
30
+ detail: detail || null, bubbles: true, cancelable: false
31
+ });
32
+ } catch (e) { ev = null; }
33
+ }
34
+ if (!ev) {
35
+ try {
36
+ ev = document.createEvent('CustomEvent');
37
+ ev.initCustomEvent(type, true, false, detail || null);
38
+ } catch (e2) { return null; }
39
+ }
40
+ el.dispatchEvent(ev);
41
+ return ev;
42
+ };
43
+ /* ==== BEHAVIOR INJECT END: emit ==== */
44
+ /* ---- 旧环境兼容:Element.closest 在老 WebView 上不存在 ----
45
+ 直接调用会在真机上抛 `is not a function`(本地完全正常,
46
+ 只在老环境炸 —— 属静默失效)。这里做模块内兜底,
47
+ 不污染全局,也不需要使用者额外引入 polyfill。 */
48
+ function closest(el, sel) {
49
+ if (!el) return null;
50
+ if (el.closest) return el.closest(sel);
51
+ while (el && el.nodeType === 1) {
52
+ if (elMatches(el, sel)) return el;
53
+ el = el.parentElement;
54
+ }
55
+ return null;
56
+ }
57
+ function elMatches(el, sel) {
58
+ var m = sel.match(/^([a-zA-Z][\w-]*)/);
59
+ if (m && el.tagName.toLowerCase() !== m[1].toLowerCase()) return false;
60
+ var cls = sel.match(/\.([\w-]+)/g);
61
+ if (cls) {
62
+ for (var i = 0; i < cls.length; i++) {
63
+ var c = cls[i].slice(1);
64
+ if ((' ' + (el.className || '') + ' ').indexOf(' ' + c + ' ') < 0) return false;
65
+ }
66
+ }
67
+ var id = sel.match(/#([\w-]+)/);
68
+ if (id && el.id !== id[1]) return false;
69
+ var at = sel.match(/\[([\w-]+)(?:=["']?([^\]"']*)["']?)?\]/);
70
+ if (at) {
71
+ var v = el.getAttribute(at[1]);
72
+ if (v === null) return false;
73
+ if (at[2] !== undefined && v !== at[2]) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ var uid = 0;
79
+
80
+ function iso(d) {
81
+ if (!d) return '';
82
+ var m = String(d).match(/^(\d{4})-(\d{2})-(\d{2})/);
83
+ if (m) return m[1] + '-' + m[2] + '-' + m[3];
84
+ var dt = new Date(d);
85
+ if (isNaN(dt)) return '';
86
+ function p(n) { return (n < 10 ? '0' : '') + n; }
87
+ return dt.getFullYear() + '-' + p(dt.getMonth() + 1) + '-' + p(dt.getDate());
88
+ }
89
+ function addDays(isoStr, n) {
90
+ var d = new Date(isoStr + 'T00:00:00');
91
+ d.setDate(d.getDate() + n);
92
+ return iso(d);
93
+ }
94
+ function addMonths(isoStr, n) {
95
+ var d = new Date(isoStr + 'T00:00:00');
96
+ var day = d.getDate();
97
+ d.setDate(1);
98
+ d.setMonth(d.getMonth() + n);
99
+ /* ⚠️ 月末溢出防护:1/31 加一个月不该变成 3/3 */
100
+ var last = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
101
+ d.setDate(Math.min(day, last));
102
+ return iso(d);
103
+ }
104
+ function quarterOf(date) {
105
+ var q = Math.floor(date.getMonth() / 3);
106
+ return { y: date.getFullYear(), q: q };
107
+ }
108
+ function quarterRange(y, q) {
109
+ /* 🔴 加防呆( H5 的单测逼出来的):
110
+ `q` 是 **0-based**(0=Q1),这是内部约定,但很容易被当成 1-based 用。
111
+ 原来传 1 会**静默返回 Q2**(不报错)—— 正是本库最厌恶的那类失效。
112
+ ⇒ 越界直接抛错,让误用在第一次就暴露。 */
113
+ if (q !== 0 && q !== 1 && q !== 2 && q !== 3) {
114
+ throw new Error('DateRange.quarterRange: 季度必须是 0-3(0=Q1),收到 ' + q);
115
+ }
116
+ var start = new Date(y, q * 3, 1);
117
+ var end = new Date(y, q * 3 + 3, 0); /* 该季度最后一天 */
118
+ return { from: iso(start), to: iso(end) };
119
+ }
120
+
121
+ function create(root, opt) {
122
+ opt = opt || {};
123
+ var from = root.querySelector('[data-dr-from]');
124
+ var to = root.querySelector('[data-dr-to]');
125
+ var errBox = root.querySelector('[data-dr-error]');
126
+ var summary = root.querySelector('[data-dr-summary]');
127
+ var clearBtn = root.querySelector('[data-dr-clear]');
128
+ if (!from || !to) throw new Error('DateRange: 缺少 [data-dr-from] / [data-dr-to]');
129
+
130
+ /* 约束:min/max 由调用方给(或默认放开)*/
131
+ if (opt.min) { from.min = opt.min; to.min = opt.min; }
132
+ if (opt.max) { from.max = opt.max; to.max = opt.max; }
133
+
134
+ function setError(msg) {
135
+ if (errBox) {
136
+ errBox.textContent = msg || '';
137
+ errBox.hidden = !msg;
138
+ }
139
+ /* ⭐ 错误要同时标记到具体字段 + 关联描述(读屏才知道哪个错了)*/
140
+ from.setAttribute('aria-invalid', msg ? 'true' : 'false');
141
+ to.setAttribute('aria-invalid', msg ? 'true' : 'false');
142
+ if (msg) {
143
+ var id = 'dr-err-' + (++uid);
144
+ errBox.id = id;
145
+ from.setAttribute('aria-describedby', id);
146
+ to.setAttribute('aria-describedby', id);
147
+ } else {
148
+ from.removeAttribute('aria-describedby');
149
+ to.removeAttribute('aria-describedby');
150
+ }
151
+ }
152
+
153
+ function days(fromS, toS) {
154
+ var a = new Date(fromS + 'T00:00:00'), b = new Date(toS + 'T00:00:00');
155
+ return Math.round((b - a) / 86400000) + 1;
156
+ }
157
+
158
+ function validate() {
159
+ var f = from.value, t = to.value;
160
+ if (!f && !t) { setError(''); paintSummary(''); return true; }
161
+ if (f && !t) { setError('请填写结束日期。'); return false; }
162
+ if (!f && t) { setError('请填写开始日期。'); return false; }
163
+ if (f > t) {
164
+ setError('开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。');
165
+ return false;
166
+ }
167
+ if (opt.min && f < opt.min) { setError('开始日期不能早于 ' + opt.min + '。'); return false; }
168
+ if (opt.max && t > opt.max) { setError('结束日期不能晚于 ' + opt.max + '。'); return false; }
169
+ setError('');
170
+ paintSummary(f + ' → ' + t + '(' + days(f, t) + ' 天)');
171
+ return true;
172
+ }
173
+
174
+ function paintSummary(s) {
175
+ if (summary) summary.innerHTML = s ? ('区间 <b>' + s + '</b>') : '';
176
+ }
177
+
178
+ function emit() {
179
+ var ok = validate();
180
+ flEmit(root, 'fl-change',
181
+ { value: { from: from.value, to: to.value }, valid: ok });
182
+ if (opt.onChange) {
183
+ opt.onChange({ from: from.value, to: to.value, valid: ok }, ok);
184
+ }
185
+ }
186
+
187
+ /* ---------- 快捷区间 ---------- */
188
+ function bindPresets() {
189
+ var now = new Date();
190
+ var q = quarterOf(now);
191
+ var presets = [
192
+ { key: 'thisQ', label: '本季度', range: function () { return quarterRange(q.y, q.q); } },
193
+ { key: 'lastQ', label: '上季度',
194
+ range: function () { return q.q === 0 ? quarterRange(q.y - 1, 3) : quarterRange(q.y, q.q - 1); } },
195
+ { key: 'lastM', label: '上月',
196
+ range: function () {
197
+ var m = new Date(now.getFullYear(), now.getMonth() - 1, 1);
198
+ return { from: iso(m), to: iso(new Date(m.getFullYear(), m.getMonth() + 1, 0)) };
199
+ } },
200
+ { key: 'thisY', label: '今年',
201
+ range: function () {
202
+ return { from: iso(new Date(now.getFullYear(), 0, 1)),
203
+ to: iso(new Date(now.getFullYear(), 11, 31)) };
204
+ } },
205
+ { key: 'yoy', label: '去年同期',
206
+ range: function () {
207
+ var y = now.getFullYear() - 1;
208
+ return { from: iso(new Date(y, 0, 1)), to: iso(new Date(y, 11, 31)) };
209
+ } },
210
+ ];
211
+ if (opt.presets) {
212
+ presets = presets.filter(function (x) { return opt.presets.indexOf(x.key) >= 0; });
213
+ }
214
+ var box = root.querySelector('[data-dr-presets]');
215
+ if (!box) return;
216
+ box.innerHTML = presets.map(function (p) {
217
+ return '<button class="drange__preset" type="button" aria-pressed="false" ' +
218
+ 'data-dr-preset="' + p.key + '">' + p.label + '</button>';
219
+ }).join('');
220
+ box.addEventListener('click', function (e) {
221
+ /* 🔴 同样是"兜底永远跑不到"的死代码(与 combobox 同一处):
222
+ `e.target.closest ? closest(...) : null` ⇒ 没有原生 closest 时返回 null。
223
+ 直接调 `closest()`,它内部自己判 `el.closest` 在不在。 */
224
+ var b = closest(e.target, '[data-dr-preset]');
225
+ if (!b) return;
226
+ var p = presets.filter(function (x) { return x.key === b.getAttribute('data-dr-preset'); })[0];
227
+ if (!p) return;
228
+ var r = p.range();
229
+ from.value = r.from;
230
+ to.value = r.to;
231
+ /* ⭐ 标记哪个快捷项是"当前"的(aria-pressed,非仅颜色)*/
232
+ box.querySelectorAll('[data-dr-preset]').forEach(function (x) {
233
+ x.setAttribute('aria-pressed', x === b ? 'true' : 'false');
234
+ });
235
+ emit();
236
+ });
237
+ }
238
+ bindPresets();
239
+
240
+ /* ---------- 清除(① 最容易漏的)---------- */
241
+ if (clearBtn) {
242
+ clearBtn.addEventListener('click', function () {
243
+ from.value = '';
244
+ to.value = '';
245
+ var box = root.querySelector('[data-dr-presets]');
246
+ if (box) {
247
+ box.querySelectorAll('[data-dr-preset]').forEach(function (x) {
248
+ x.setAttribute('aria-pressed', 'false');
249
+ });
250
+ }
251
+ setError('');
252
+ paintSummary('');
253
+ emit();
254
+ from.focus(); /* 清完把焦点放回开始日期 */
255
+ });
256
+ }
257
+
258
+ /* ---------- 手动改日期 ---------- */
259
+ [from, to].forEach(function (el) {
260
+ el.addEventListener('change', function () {
261
+ var box = root.querySelector('[data-dr-presets]');
262
+ if (box) {
263
+ box.querySelectorAll('[data-dr-preset]').forEach(function (x) {
264
+ x.setAttribute('aria-pressed', 'false');
265
+ });
266
+ }
267
+ emit();
268
+ });
269
+ el.addEventListener('input', emit);
270
+ });
271
+
272
+ /* ---------- 便捷 API ---------- */
273
+ function setRange(a, b2) {
274
+ from.value = iso(a);
275
+ to.value = iso(b2);
276
+ emit();
277
+ }
278
+
279
+ validate();
280
+ return {
281
+ get from() { return from.value; },
282
+ get to() { return to.value; },
283
+ get valid() { return !errBox || errBox.hidden; },
284
+ set: setRange,
285
+ setPreset: function (key) {
286
+ var b = root.querySelector('[data-dr-preset="' + key + '"]');
287
+ if (b) b.click();
288
+ },
289
+ clear: function () { if (clearBtn) clearBtn.click(); },
290
+ };
291
+ }
292
+
293
+ global.DateRange = {
294
+ create: create,
295
+ /* 🔴 补上 quarterOf( H5 的单测抓出来的):
296
+ 它一直是**内部函数但没导出** ⇒ 使用者拿到 quarterRange 也无法
297
+ 从任意日期反推季度号。纯函数,导出无害且有用。 */
298
+ iso: iso, addDays: addDays, addMonths: addMonths,
299
+ quarterOf: quarterOf, quarterRange: quarterRange,
300
+ };
301
+ })(window);