@icen.ai/ui 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +79 -309
- package/LICENSE +21 -0
- package/README.md +43 -54
- package/dist/base.css +68 -22
- package/dist/behaviors/accordion.mjs +1 -1
- package/dist/behaviors/ai-branch.mjs +14 -0
- package/dist/behaviors/ai-chat.d.mts +2 -2
- package/dist/behaviors/ai-chat.mjs +66 -10
- package/dist/behaviors/ai-composer.d.mts +5 -5
- package/dist/behaviors/ai-composer.mjs +34 -10
- package/dist/behaviors/ai-core.mjs +14 -0
- package/dist/behaviors/ai-diff.mjs +14 -0
- package/dist/behaviors/ai-feedback.mjs +14 -0
- package/dist/behaviors/ai-panel.d.mts +2 -2
- package/dist/behaviors/ai-panel.mjs +14 -0
- package/dist/behaviors/ai-provider.d.mts +1 -1
- package/dist/behaviors/ai-provider.mjs +14 -0
- package/dist/behaviors/ai-threads.mjs +14 -0
- package/dist/behaviors/ai-tool.d.mts +1 -1
- package/dist/behaviors/ai-tool.mjs +299 -24
- package/dist/behaviors/ai-tools.mjs +1179 -28
- package/dist/behaviors/autocomplete.d.mts +1 -1
- package/dist/behaviors/autocomplete.mjs +14 -0
- package/dist/behaviors/back-top.mjs +14 -0
- package/dist/behaviors/carousel.mjs +21 -1
- package/dist/behaviors/cascader.mjs +27 -0
- package/dist/behaviors/charts.d.mts +12 -1
- package/dist/behaviors/charts.mjs +298 -20
- package/dist/behaviors/code-block.mjs +22 -1
- package/dist/behaviors/command-palette.mjs +3 -3
- package/dist/behaviors/context-menu.d.mts +2 -2
- package/dist/behaviors/context-menu.mjs +14 -0
- package/dist/behaviors/controls.d.mts +8 -8
- package/dist/behaviors/controls.mjs +50 -6
- package/dist/behaviors/copy.mjs +14 -0
- package/dist/behaviors/datatable.d.mts +3 -3
- package/dist/behaviors/datatable.mjs +89 -11
- package/dist/behaviors/date-picker.mjs +14 -0
- package/dist/behaviors/events.d.mts +10 -10
- package/dist/behaviors/events.mjs +11 -1
- package/dist/behaviors/form-validate.d.mts +10 -1
- package/dist/behaviors/form-validate.mjs +279 -4611
- package/dist/behaviors/i18n.d.mts +14 -0
- package/dist/behaviors/i18n.mjs +28 -0
- package/dist/behaviors/input.mjs +16 -0
- package/dist/behaviors/kb-access.d.mts +2 -2
- package/dist/behaviors/kb-access.mjs +21 -2
- package/dist/behaviors/kb-acl.d.mts +2 -2
- package/dist/behaviors/kb-acl.mjs +14 -0
- package/dist/behaviors/kb-answer.d.mts +2 -2
- package/dist/behaviors/kb-answer.mjs +298 -23
- package/dist/behaviors/kb-audit.d.mts +2 -2
- package/dist/behaviors/kb-audit.mjs +14 -0
- package/dist/behaviors/kb-canvas.d.mts +3 -3
- package/dist/behaviors/kb-canvas.mjs +14 -0
- package/dist/behaviors/kb-chain.d.mts +2 -2
- package/dist/behaviors/kb-chain.mjs +14 -0
- package/dist/behaviors/kb-checkpoint.d.mts +2 -2
- package/dist/behaviors/kb-checkpoint.mjs +14 -0
- package/dist/behaviors/kb-chunks.d.mts +2 -2
- package/dist/behaviors/kb-chunks.mjs +14 -0
- package/dist/behaviors/kb-citation.d.mts +2 -2
- package/dist/behaviors/kb-citation.mjs +33 -6
- package/dist/behaviors/kb-clarify.d.mts +2 -2
- package/dist/behaviors/kb-clarify.mjs +14 -0
- package/dist/behaviors/kb-conflict.d.mts +2 -2
- package/dist/behaviors/kb-conflict.mjs +14 -0
- package/dist/behaviors/kb-connector.d.mts +2 -2
- package/dist/behaviors/kb-connector.mjs +14 -0
- package/dist/behaviors/kb-core.d.mts +1 -1
- package/dist/behaviors/kb-core.mjs +14 -0
- package/dist/behaviors/kb-eval.d.mts +2 -2
- package/dist/behaviors/kb-eval.mjs +21 -2
- package/dist/behaviors/kb-explain.d.mts +2 -2
- package/dist/behaviors/kb-explain.mjs +26 -2
- package/dist/behaviors/kb-filter.d.mts +2 -2
- package/dist/behaviors/kb-filter.mjs +21 -2
- package/dist/behaviors/kb-gap.d.mts +2 -2
- package/dist/behaviors/kb-gap.mjs +14 -0
- package/dist/behaviors/kb-hittest.d.mts +2 -2
- package/dist/behaviors/kb-hittest.mjs +15 -1
- package/dist/behaviors/kb-metadata.mjs +19 -0
- package/dist/behaviors/kb-passage.d.mts +2 -2
- package/dist/behaviors/kb-passage.mjs +14 -0
- package/dist/behaviors/kb-pipeline.d.mts +2 -2
- package/dist/behaviors/kb-pipeline.mjs +14 -0
- package/dist/behaviors/kb-qa.mjs +14 -0
- package/dist/behaviors/kb-rerank.d.mts +2 -2
- package/dist/behaviors/kb-rerank.mjs +14 -0
- package/dist/behaviors/kb-retrieval.d.mts +2 -2
- package/dist/behaviors/kb-retrieval.mjs +31 -4
- package/dist/behaviors/kb-review.d.mts +2 -2
- package/dist/behaviors/kb-review.mjs +21 -2
- package/dist/behaviors/kb-sandbox.d.mts +2 -2
- package/dist/behaviors/kb-sandbox.mjs +20 -1
- package/dist/behaviors/kb-segment.d.mts +2 -2
- package/dist/behaviors/kb-segment.mjs +15 -1
- package/dist/behaviors/kb-select.mjs +21 -2
- package/dist/behaviors/kb-sources.d.mts +2 -2
- package/dist/behaviors/kb-sources.mjs +14 -0
- package/dist/behaviors/kb-sql.d.mts +2 -2
- package/dist/behaviors/kb-sql.mjs +14 -0
- package/dist/behaviors/kb-trace.d.mts +2 -2
- package/dist/behaviors/kb-trace.mjs +22 -3
- package/dist/behaviors/kb-visibility.d.mts +2 -2
- package/dist/behaviors/kb-visibility.mjs +14 -0
- package/dist/behaviors/kb-who-can.d.mts +2 -2
- package/dist/behaviors/kb-who-can.mjs +21 -2
- package/dist/behaviors/mentions.d.mts +1 -1
- package/dist/behaviors/mentions.mjs +14 -0
- package/dist/behaviors/modal.d.mts +1 -1
- package/dist/behaviors/modal.mjs +14 -0
- package/dist/behaviors/motion.d.mts +16 -0
- package/dist/behaviors/motion.mjs +39 -0
- package/dist/behaviors/notification.d.mts +2 -2
- package/dist/behaviors/notification.mjs +27 -3
- package/dist/behaviors/panel.mjs +14 -0
- package/dist/behaviors/scroll-area.d.mts +17 -0
- package/dist/behaviors/scroll-area.mjs +59 -0
- package/dist/behaviors/select.mjs +21 -2
- package/dist/behaviors/slider.mjs +3 -3
- package/dist/behaviors/split-pane.mjs +2 -0
- package/dist/behaviors/suggestions.mjs +14 -0
- package/dist/behaviors/tabs.mjs +10 -0
- package/dist/behaviors/tag-input.mjs +14 -0
- package/dist/behaviors/theme-switcher.d.mts +2 -2
- package/dist/behaviors/theme-switcher.mjs +30 -2
- package/dist/behaviors/theme.mjs +16 -2
- package/dist/behaviors/toast.d.mts +2 -2
- package/dist/behaviors/toast.mjs +14 -0
- package/dist/behaviors/tree-select.mjs +27 -0
- package/dist/behaviors/tree.mjs +4 -4
- package/dist/behaviors/upload.mjs +14 -0
- package/dist/behaviors/url-safety.d.mts +8 -0
- package/dist/behaviors/url-safety.mjs +11 -0
- package/dist/components/accordion.d.mts +2 -0
- package/dist/components/ai-branch.d.mts +2 -0
- package/dist/components/ai-chat.css +40 -33
- package/dist/components/ai-chat.d.mts +2 -0
- package/dist/components/ai-composer.d.mts +2 -0
- package/dist/components/ai-context.d.mts +2 -0
- package/dist/components/ai-context.mjs +1 -0
- package/dist/components/ai-diff.css +6 -6
- package/dist/components/ai-diff.d.mts +2 -0
- package/dist/components/ai-feedback.d.mts +2 -0
- package/dist/components/ai-files.d.mts +2 -0
- package/dist/components/ai-message.d.mts +2 -0
- package/dist/components/ai-panel.css +12 -10
- package/dist/components/ai-reasoning.d.mts +2 -0
- package/dist/components/ai-subagent.d.mts +2 -0
- package/dist/components/ai-subagent.mjs +1 -0
- package/dist/components/ai-threads.d.mts +2 -0
- package/dist/components/ai-todo.d.mts +2 -0
- package/dist/components/ai-todo.mjs +1 -0
- package/dist/components/ai-tool-call.d.mts +2 -0
- package/dist/components/ai-tool-call.mjs +1 -0
- package/dist/components/ai-tool.css +16 -16
- package/dist/components/ai-usage.d.mts +2 -0
- package/dist/components/alert.d.mts +2 -0
- package/dist/components/autocomplete.css +12 -12
- package/dist/components/autocomplete.d.mts +2 -0
- package/dist/components/avatar.d.mts +2 -0
- package/dist/components/back-top.css +47 -16
- package/dist/components/back-top.d.mts +2 -0
- package/dist/components/badge.css +1 -1
- package/dist/components/badge.d.mts +2 -0
- package/dist/components/breadcrumb.css +26 -8
- package/dist/components/breadcrumb.d.mts +2 -0
- package/dist/components/btn.css +67 -62
- package/dist/components/btn.d.mts +2 -0
- package/dist/components/card.css +22 -22
- package/dist/components/card.d.mts +2 -0
- package/dist/components/carousel.d.mts +2 -0
- package/dist/components/cascader.css +56 -35
- package/dist/components/cascader.d.mts +2 -0
- package/dist/components/chart-area.css +46 -0
- package/dist/components/chart-area.d.mts +2 -0
- package/dist/components/chart-bar.css +66 -19
- package/dist/components/chart-bar.d.mts +2 -0
- package/dist/components/chart-calendar.d.mts +2 -0
- package/dist/components/chart-gauge.css +23 -3
- package/dist/components/chart-gauge.d.mts +2 -0
- package/dist/components/chart-graph.css +15 -3
- package/dist/components/chart-graph.d.mts +2 -0
- package/dist/components/chart-heatmap.css +117 -95
- package/dist/components/chart-heatmap.d.mts +2 -0
- package/dist/components/chart-line.css +54 -0
- package/dist/components/chart-line.d.mts +2 -0
- package/dist/components/chart-map.d.mts +2 -0
- package/dist/components/chart-pie.css +29 -0
- package/dist/components/chart-pie.d.mts +2 -0
- package/dist/components/chart-radar.css +50 -1
- package/dist/components/chart-radar.d.mts +2 -0
- package/dist/components/chart-scatter.css +39 -2
- package/dist/components/chart-scatter.d.mts +2 -0
- package/dist/components/chart-sparkline.css +20 -0
- package/dist/components/chart-sparkline.d.mts +2 -0
- package/dist/components/chart-stack.css +16 -5
- package/dist/components/chart-stack.d.mts +2 -0
- package/dist/components/charts.css +109 -35
- package/dist/components/charts.d.mts +2 -0
- package/dist/components/code-block.css +10 -8
- package/dist/components/code-block.d.mts +2 -0
- package/dist/components/command-palette.css +45 -38
- package/dist/components/command-palette.d.mts +2 -0
- package/dist/components/content.css +62 -57
- package/dist/components/context-menu.d.mts +2 -0
- package/dist/components/copy.css +10 -10
- package/dist/components/copy.d.mts +2 -0
- package/dist/components/datatable.css +72 -59
- package/dist/components/datatable.d.mts +2 -0
- package/dist/components/date-picker.css +26 -26
- package/dist/components/date-picker.d.mts +2 -0
- package/dist/components/desc.d.mts +2 -0
- package/dist/components/dropdown.d.mts +2 -0
- package/dist/components/empty.css +16 -16
- package/dist/components/empty.d.mts +2 -0
- package/dist/components/feedback.css +31 -29
- package/dist/components/form-validate.css +20 -6
- package/dist/components/form-validate.d.mts +2 -0
- package/dist/components/form.css +17 -14
- package/dist/components/form.d.mts +2 -0
- package/dist/components/icon.css +21 -10
- package/dist/components/icon.d.mts +2 -0
- package/dist/components/input.css +22 -22
- package/dist/components/input.d.mts +2 -0
- package/dist/components/kb-access.d.mts +2 -0
- package/dist/components/kb-acl.d.mts +2 -0
- package/dist/components/kb-agent.css +134 -55
- package/dist/components/kb-answer.d.mts +2 -0
- package/dist/components/kb-audit.d.mts +2 -0
- package/dist/components/kb-canvas.d.mts +2 -0
- package/dist/components/kb-chain.d.mts +2 -0
- package/dist/components/kb-checkpoint.d.mts +2 -0
- package/dist/components/kb-chunks.d.mts +2 -0
- package/dist/components/kb-citation.d.mts +2 -0
- package/dist/components/kb-clarify.d.mts +2 -0
- package/dist/components/kb-conflict.d.mts +2 -0
- package/dist/components/kb-connector.d.mts +2 -0
- package/dist/components/kb-data.css +192 -80
- package/dist/components/kb-eval.d.mts +2 -0
- package/dist/components/kb-explain.d.mts +2 -0
- package/dist/components/kb-filter.d.mts +2 -0
- package/dist/components/kb-gap.d.mts +2 -0
- package/dist/components/kb-ground.css +165 -73
- package/dist/components/kb-hittest.d.mts +2 -0
- package/dist/components/kb-ingest.css +308 -113
- package/dist/components/kb-metadata.d.mts +2 -0
- package/dist/components/kb-ops.css +189 -75
- package/dist/components/kb-passage.d.mts +2 -0
- package/dist/components/kb-perm.css +158 -87
- package/dist/components/kb-pipeline.d.mts +2 -0
- package/dist/components/kb-qa.d.mts +2 -0
- package/dist/components/kb-rerank.d.mts +2 -0
- package/dist/components/kb-retrieval.d.mts +2 -0
- package/dist/components/kb-review.d.mts +2 -0
- package/dist/components/kb-sandbox.d.mts +2 -0
- package/dist/components/kb-search.css +353 -116
- package/dist/components/kb-segment.d.mts +2 -0
- package/dist/components/kb-sources.d.mts +2 -0
- package/dist/components/kb-sql.d.mts +2 -0
- package/dist/components/kb-trace.d.mts +2 -0
- package/dist/components/kb-visibility.d.mts +2 -0
- package/dist/components/kb-who-can.d.mts +2 -0
- package/dist/components/kb.css +60 -36
- package/dist/components/kb.d.mts +2 -0
- package/dist/components/kbd.d.mts +2 -0
- package/dist/components/layout.css +10 -10
- package/dist/components/layout.d.mts +2 -0
- package/dist/components/list.d.mts +2 -0
- package/dist/components/material-chinese.css +10 -0
- package/dist/components/material-soviet.css +10 -0
- package/dist/components/material.css +61 -18
- package/dist/components/media-card.d.mts +2 -0
- package/dist/components/media.css +22 -27
- package/dist/components/mentions.css +18 -17
- package/dist/components/mentions.d.mts +2 -0
- package/dist/components/menu.css +32 -28
- package/dist/components/modal.css +30 -27
- package/dist/components/modal.d.mts +2 -0
- package/dist/components/nav.css +48 -44
- package/dist/components/nav.d.mts +2 -0
- package/dist/components/notification.css +31 -25
- package/dist/components/notification.d.mts +2 -0
- package/dist/components/pagination.css +15 -13
- package/dist/components/pagination.d.mts +2 -0
- package/dist/components/panel.css +19 -20
- package/dist/components/panel.d.mts +2 -0
- package/dist/components/pill.css +8 -7
- package/dist/components/pill.d.mts +2 -0
- package/dist/components/popover.css +32 -23
- package/dist/components/popover.d.mts +2 -0
- package/dist/components/progress.d.mts +2 -0
- package/dist/components/rating.d.mts +2 -0
- package/dist/components/result.d.mts +2 -0
- package/dist/components/scroll-area.css +84 -112
- package/dist/components/scroll-area.d.mts +2 -0
- package/dist/components/scroll-area.mjs +1 -0
- package/dist/components/segmented.css +47 -29
- package/dist/components/segmented.d.mts +2 -0
- package/dist/components/select.css +80 -41
- package/dist/components/select.d.mts +2 -0
- package/dist/components/sidebar.css +42 -38
- package/dist/components/sidebar.d.mts +2 -0
- package/dist/components/skeleton.d.mts +2 -0
- package/dist/components/slider.css +26 -22
- package/dist/components/slider.d.mts +2 -0
- package/dist/components/spinner.d.mts +2 -0
- package/dist/components/split-pane.css +16 -7
- package/dist/components/split-pane.d.mts +2 -0
- package/dist/components/stat.css +17 -16
- package/dist/components/stat.d.mts +2 -0
- package/dist/components/steps.css +26 -26
- package/dist/components/steps.d.mts +2 -0
- package/dist/components/switch.css +78 -39
- package/dist/components/switch.d.mts +2 -0
- package/dist/components/table.css +54 -32
- package/dist/components/table.d.mts +2 -0
- package/dist/components/tabs.css +53 -32
- package/dist/components/tabs.d.mts +2 -0
- package/dist/components/tag-input.css +12 -8
- package/dist/components/tag-input.d.mts +2 -0
- package/dist/components/tag.css +20 -12
- package/dist/components/tag.d.mts +2 -0
- package/dist/components/theme-switcher.css +7 -7
- package/dist/components/theme-switcher.d.mts +2 -0
- package/dist/components/timeline.d.mts +2 -0
- package/dist/components/toast.css +34 -30
- package/dist/components/toast.d.mts +2 -0
- package/dist/components/toolbar.css +19 -18
- package/dist/components/toolbar.d.mts +2 -0
- package/dist/components/tooltip.css +9 -8
- package/dist/components/tooltip.d.mts +2 -0
- package/dist/components/tree-select.css +63 -40
- package/dist/components/tree-select.d.mts +2 -0
- package/dist/components/tree.d.mts +2 -0
- package/dist/components/upload.css +9 -11
- package/dist/components/upload.d.mts +2 -0
- package/dist/index.d.mts +85 -84
- package/dist/index.mjs +14644 -13935
- package/dist/react.d.mts +18 -18
- package/dist/react.mjs +377 -36
- package/dist/registry.json +797 -64
- package/dist/tokens/colors.css +6 -0
- package/dist/tokens/design.css +137 -31
- package/dist/tokens/dtcg.json +9592 -2008
- package/dist/tokens/figma.variables.json +7451 -2141
- package/dist/tokens/motion.css +51 -0
- package/dist/tokens/pixel.css +10 -0
- package/dist/tokens/scrollbars.css +40 -0
- package/dist/tokens/tokens-studio.json +8390 -1142
- package/dist/tokens/typography.css +33 -0
- package/dist/tokens.css +402 -82
- package/dist/ui.css +4196 -2163
- package/dist/vue.d.mts +17 -17
- package/dist/vue.mjs +371 -34
- package/docs/spec/charts-scale-a11y.md +115 -0
- package/docs/spec/design-philosophy.md +5 -5
- package/docs/spec/design-primitives.md +10 -10
- package/docs/spec/motion-typography.md +105 -0
- package/docs/spec/scrollbars.md +46 -0
- package/package.json +17 -3
|
@@ -116,6 +116,7 @@ var ICEN_LOCALE_ZH_CN = {
|
|
|
116
116
|
"aiChat.roleTool": "\u5DE5",
|
|
117
117
|
"aiChat.roleUser": "\u6211",
|
|
118
118
|
"aiChat.thinking": "\u601D\u8003 {p0}",
|
|
119
|
+
"aiChat.thinkingNow": "\u6B63\u5728\u601D\u8003\u2026",
|
|
119
120
|
/* ── aiComposer ── */
|
|
120
121
|
"aiComposer.attachAgent": "\u4EE3\u7406",
|
|
121
122
|
"aiComposer.library": "\u6DFB\u52A0\u4E0A\u4E0B\u6587\u4E0E\u547D\u4EE4",
|
|
@@ -298,6 +299,19 @@ var ICEN_LOCALE_ZH_CN = {
|
|
|
298
299
|
"charts.noData": "\u6682\u65E0\u6570\u636E",
|
|
299
300
|
"charts.resetZoom": "\u91CD\u7F6E\u7F29\u653E",
|
|
300
301
|
"charts.sampled": "\u5DF2\u62BD\u6837 {p0}\u2192{p1} \u70B9",
|
|
302
|
+
"charts.a11y.keyboardHint": "\u65B9\u5411\u952E\u9010\u70B9\u6D4F\u89C8\uFF0CHome/End \u8DF3\u9996\u5C3E\uFF0CEsc \u91CD\u7F6E\u7F29\u653E",
|
|
303
|
+
"charts.a11y.max": "\u6700\u5927 {p0}\uFF08{p1}\uFF09",
|
|
304
|
+
"charts.a11y.min": "\u6700\u5C0F {p0}\uFF08{p1}\uFF09",
|
|
305
|
+
"charts.a11y.point": "{p0}\uFF1A{p1}\uFF0C\u7B2C {p2}/{p3} \u70B9",
|
|
306
|
+
"charts.a11y.pointSeries": "{p0} \xB7 {p1}\uFF1A{p2}\uFF0C\u7B2C {p3}/{p4} \u70B9",
|
|
307
|
+
"charts.a11y.range": "\u7C7B\u76EE {p0} \u81F3 {p1}",
|
|
308
|
+
"charts.a11y.scatterRange": "x \u8303\u56F4 {p0}\u2013{p1}\uFF0Cy \u8303\u56F4 {p2}\u2013{p3}",
|
|
309
|
+
"charts.a11y.seriesCount": "{p0} \u4E2A\u7CFB\u5217",
|
|
310
|
+
"charts.a11y.tableCaption": "{p0} \u6570\u636E\u8868",
|
|
311
|
+
"charts.a11y.trend": "\u9996\u7CFB\u5217\u8D8B\u52BF{p0}",
|
|
312
|
+
"charts.a11y.trendDown": "\u4E0B\u964D",
|
|
313
|
+
"charts.a11y.trendFlat": "\u6301\u5E73",
|
|
314
|
+
"charts.a11y.trendUp": "\u4E0A\u5347",
|
|
301
315
|
"charts.type.area": "\u9762\u79EF\u56FE",
|
|
302
316
|
"charts.type.donut": "\u73AF\u5F62\u56FE",
|
|
303
317
|
"charts.type.gauge": "\u4EEA\u8868\u76D8",
|
|
@@ -1163,7 +1177,8 @@ function withChrome(el, opts, hasLegend, paint, state, extras) {
|
|
|
1163
1177
|
emitIcen(el, "icen:chart-legend-visibility", { visible: hidden ? false : true });
|
|
1164
1178
|
});
|
|
1165
1179
|
syncEye();
|
|
1166
|
-
head.append(title, eye);
|
|
1180
|
+
if (hasLegend) head.append(title, eye);
|
|
1181
|
+
else head.append(title);
|
|
1167
1182
|
el.append(head, body);
|
|
1168
1183
|
paint(body);
|
|
1169
1184
|
}
|
|
@@ -1264,7 +1279,7 @@ function renderVBar(el, opts) {
|
|
|
1264
1279
|
wrap.appendChild(track);
|
|
1265
1280
|
}
|
|
1266
1281
|
body.append(wrap, labelsRow(labelsUsed));
|
|
1267
|
-
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name
|
|
1282
|
+
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: `${si}:${s.name}`, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
|
|
1268
1283
|
});
|
|
1269
1284
|
attachChartInteraction(el, "vbar");
|
|
1270
1285
|
return el;
|
|
@@ -1359,8 +1374,10 @@ function renderDonut(el, opts) {
|
|
|
1359
1374
|
"stroke-width": "14"
|
|
1360
1375
|
}));
|
|
1361
1376
|
let offset = 0;
|
|
1377
|
+
const gap = opts.segments.length > 1 ? 2.5 : 0;
|
|
1362
1378
|
opts.segments.forEach((seg, segIndex) => {
|
|
1363
|
-
const
|
|
1379
|
+
const arcLength = Math.max(0, seg.value / total * circumference);
|
|
1380
|
+
const dash = arcLength - Math.min(gap, arcLength * 0.1);
|
|
1364
1381
|
const arc = svgEl("circle", {
|
|
1365
1382
|
cx: "60",
|
|
1366
1383
|
cy: "60",
|
|
@@ -1374,7 +1391,7 @@ function renderDonut(el, opts) {
|
|
|
1374
1391
|
});
|
|
1375
1392
|
setMarkData(arc, { index: segIndex, label: seg.label, value: seg.value });
|
|
1376
1393
|
svg.appendChild(arc);
|
|
1377
|
-
offset +=
|
|
1394
|
+
offset += arcLength;
|
|
1378
1395
|
});
|
|
1379
1396
|
const legend = document.createElement("div");
|
|
1380
1397
|
legend.className = "icen-chart-legend";
|
|
@@ -1409,8 +1426,9 @@ function renderLine(el, opts) {
|
|
|
1409
1426
|
const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chartAriaLabel(opts.title, t("charts.type.line")) });
|
|
1410
1427
|
const defs = svgEl("defs", {});
|
|
1411
1428
|
for (let i = 0; i < 4; i++) {
|
|
1412
|
-
const y = pad + i * (height - pad * 2) / 3;
|
|
1429
|
+
const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
|
|
1413
1430
|
svg.appendChild(svgEl("line", {
|
|
1431
|
+
class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
|
|
1414
1432
|
x1: String(pad),
|
|
1415
1433
|
x2: String(width - pad),
|
|
1416
1434
|
y1: String(y),
|
|
@@ -1494,7 +1512,7 @@ function renderLine(el, opts) {
|
|
|
1494
1512
|
if (defs.firstChild) svg.insertBefore(defs, svg.firstChild);
|
|
1495
1513
|
wrap.append(svg, pointLabelsRow(labelsUsed, width, pad));
|
|
1496
1514
|
body.appendChild(wrap);
|
|
1497
|
-
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name
|
|
1515
|
+
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: `${si}:${s.name}`, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
|
|
1498
1516
|
});
|
|
1499
1517
|
if (el.dataset.icenChartBody == null) observeFluidWidth(el, () => renderLine(el, opts));
|
|
1500
1518
|
attachChartInteraction(el, "line");
|
|
@@ -1601,7 +1619,7 @@ function renderRadar(el, opts) {
|
|
|
1601
1619
|
setMarkData(dot, { index: i, seriesIndex: si, seriesName: s.name || void 0, label: axes[i] ?? "", value: s.values[i] ?? 0 });
|
|
1602
1620
|
svg.appendChild(dot);
|
|
1603
1621
|
}
|
|
1604
|
-
legendItems.push({ key: s.name || t("charts.seriesName", { p0: si + 1 })
|
|
1622
|
+
legendItems.push({ key: `${si}:${s.name || t("charts.seriesName", { p0: si + 1 })}`, label: s.name || t("charts.seriesName", { p0: si + 1 }), tone: s.tone, si });
|
|
1605
1623
|
});
|
|
1606
1624
|
wrap.appendChild(svg);
|
|
1607
1625
|
body.appendChild(wrap);
|
|
@@ -1623,6 +1641,11 @@ function niceCeil(v) {
|
|
|
1623
1641
|
else nice = 10;
|
|
1624
1642
|
return nice * base;
|
|
1625
1643
|
}
|
|
1644
|
+
function tickPrecision(min, max, steps) {
|
|
1645
|
+
const span = Math.abs(max - min);
|
|
1646
|
+
if (!Number.isFinite(span) || span <= 0) return 0;
|
|
1647
|
+
return Math.max(0, Math.ceil(-Math.log10(span / steps)));
|
|
1648
|
+
}
|
|
1626
1649
|
function isoDate(d) {
|
|
1627
1650
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1628
1651
|
const day = String(d.getDate()).padStart(2, "0");
|
|
@@ -1718,7 +1741,8 @@ function renderSparkline(el, opts) {
|
|
|
1718
1741
|
const tone = toneVar(opts.tone);
|
|
1719
1742
|
const points = values.map((value, i) => ({
|
|
1720
1743
|
x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
|
|
1721
|
-
|
|
1744
|
+
/* 负值钳到基线(与折线图同口径):viewBox 仅 28 高,反向射出更刺眼 */
|
|
1745
|
+
y: height - pad - Math.max(0, value) / max * (height - pad * 2),
|
|
1722
1746
|
value
|
|
1723
1747
|
}));
|
|
1724
1748
|
const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
|
|
@@ -1843,6 +1867,7 @@ function renderCalendar(el, opts) {
|
|
|
1843
1867
|
weekdayRow.appendChild(s);
|
|
1844
1868
|
});
|
|
1845
1869
|
const max = maxOf(entries.map((e) => e.value));
|
|
1870
|
+
entries.sort((a, b) => a.date.localeCompare(b.date));
|
|
1846
1871
|
const pad = padFirstWeek(grid, entries[0].date, weekStart);
|
|
1847
1872
|
let lastMonth = -1;
|
|
1848
1873
|
entries.forEach((entry, i) => {
|
|
@@ -1901,8 +1926,9 @@ function renderScatter(el, opts) {
|
|
|
1901
1926
|
const xAt = (x) => pad + (x - xMin) / (xMax - xMin) * (width - pad * 2);
|
|
1902
1927
|
const yAt = (y) => height - pad - (y - yMin) / (yMax - yMin) * (height - pad * 2);
|
|
1903
1928
|
for (let i = 0; i <= 3; i++) {
|
|
1904
|
-
const y = pad + i * (height - pad * 2) / 3;
|
|
1929
|
+
const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
|
|
1905
1930
|
svg.appendChild(svgEl("line", {
|
|
1931
|
+
class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
|
|
1906
1932
|
x1: String(pad),
|
|
1907
1933
|
x2: String(width - pad),
|
|
1908
1934
|
y1: String(y),
|
|
@@ -1918,12 +1944,13 @@ function renderScatter(el, opts) {
|
|
|
1918
1944
|
"font-size": "8",
|
|
1919
1945
|
fill: "var(--icen-token-text-faint)"
|
|
1920
1946
|
});
|
|
1921
|
-
txt.textContent = fmt(opts,
|
|
1947
|
+
txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(yMin, yMax, 3)));
|
|
1922
1948
|
svg.appendChild(txt);
|
|
1923
1949
|
}
|
|
1924
1950
|
for (let i = 0; i <= 4; i++) {
|
|
1925
|
-
const x = pad + i * (width - pad * 2) / 4;
|
|
1951
|
+
const x = Math.round(pad + i * (width - pad * 2) / 4) + 0.5;
|
|
1926
1952
|
svg.appendChild(svgEl("line", {
|
|
1953
|
+
class: i === 0 ? "icen-chart-grid icen-chart-grid--v icen-chart-axis" : "icen-chart-grid icen-chart-grid--v",
|
|
1927
1954
|
x1: String(x),
|
|
1928
1955
|
x2: String(x),
|
|
1929
1956
|
y1: String(pad),
|
|
@@ -1940,7 +1967,7 @@ function renderScatter(el, opts) {
|
|
|
1940
1967
|
"font-size": "8",
|
|
1941
1968
|
fill: "var(--icen-token-text-faint)"
|
|
1942
1969
|
});
|
|
1943
|
-
txt.textContent = fmt(opts,
|
|
1970
|
+
txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(xMin, xMax, 4)));
|
|
1944
1971
|
svg.appendChild(txt);
|
|
1945
1972
|
}
|
|
1946
1973
|
if (opts.yLabel) {
|
|
@@ -2177,6 +2204,14 @@ function normalizeChartSpec(raw) {
|
|
|
2177
2204
|
}
|
|
2178
2205
|
if (s.zoom === "x" || s.zoom === "brush") spec.zoom = s.zoom;
|
|
2179
2206
|
if (typeof s.loading === "boolean") spec.loading = s.loading;
|
|
2207
|
+
if (s.a11y && typeof s.a11y === "object") {
|
|
2208
|
+
const a = s.a11y;
|
|
2209
|
+
const a11y = {};
|
|
2210
|
+
if (typeof a.summary === "boolean") a11y.summary = a.summary;
|
|
2211
|
+
if (typeof a.table === "boolean") a11y.table = a.table;
|
|
2212
|
+
if (typeof a.keyboard === "boolean") a11y.keyboard = a.keyboard;
|
|
2213
|
+
if (Object.keys(a11y).length > 0) spec.a11y = a11y;
|
|
2214
|
+
}
|
|
2180
2215
|
if (Array.isArray(s.marks)) {
|
|
2181
2216
|
const marks = [];
|
|
2182
2217
|
for (const raw2 of s.marks) {
|
|
@@ -2304,6 +2339,7 @@ function renderChartSkeleton(body) {
|
|
|
2304
2339
|
body.appendChild(box);
|
|
2305
2340
|
}
|
|
2306
2341
|
var CHART_MARK_ACCENT = "var(--icen-token-accent)";
|
|
2342
|
+
var CHART_MARK_ACCENT_TEXT = "var(--icen-token-accent-text)";
|
|
2307
2343
|
function markToneVar(tone) {
|
|
2308
2344
|
switch (tone) {
|
|
2309
2345
|
case "success":
|
|
@@ -2439,7 +2475,7 @@ function paintSvgChartMarks(root, view, marks, formatValue, warned) {
|
|
|
2439
2475
|
y: String(clampNum(y - 10, 8, CHART_LINE_H - 4)),
|
|
2440
2476
|
"text-anchor": "middle",
|
|
2441
2477
|
"font-size": "8",
|
|
2442
|
-
fill:
|
|
2478
|
+
fill: CHART_MARK_ACCENT_TEXT
|
|
2443
2479
|
});
|
|
2444
2480
|
label.textContent = mark.text ?? mark.label;
|
|
2445
2481
|
g.appendChild(label);
|
|
@@ -2531,6 +2567,81 @@ function paintVBarChartMarks(root, view, spec, marks, formatValue, warned) {
|
|
|
2531
2567
|
function chartAriaLabel(title, typeName) {
|
|
2532
2568
|
return title ? `${typeName}\uFF1A${title}` : typeName;
|
|
2533
2569
|
}
|
|
2570
|
+
function summarizeChart(view, type, fmt2) {
|
|
2571
|
+
const series = view.series && view.series.length > 0 ? view.series : [{ name: "", values: view.values ?? [] }];
|
|
2572
|
+
const parts = [];
|
|
2573
|
+
if (series.length > 1) parts.push(t("charts.a11y.seriesCount", { p0: series.length }));
|
|
2574
|
+
if (type === "scatter") {
|
|
2575
|
+
const pts = view.points ?? [];
|
|
2576
|
+
if (pts.length > 0) {
|
|
2577
|
+
const xs = pts.map((p) => p.x);
|
|
2578
|
+
const ys = pts.map((p) => p.y);
|
|
2579
|
+
parts.push(t("charts.a11y.scatterRange", { p0: fmt2(Math.min(...xs)), p1: fmt2(Math.max(...xs)), p2: fmt2(Math.min(...ys)), p3: fmt2(Math.max(...ys)) }));
|
|
2580
|
+
}
|
|
2581
|
+
return parts.join("\uFF1B");
|
|
2582
|
+
}
|
|
2583
|
+
const labels = view.labels ?? [];
|
|
2584
|
+
if (labels.length > 1) parts.push(t("charts.a11y.range", { p0: labels[0], p1: labels[labels.length - 1] }));
|
|
2585
|
+
let min = Infinity;
|
|
2586
|
+
let max = -Infinity;
|
|
2587
|
+
let minAt = "";
|
|
2588
|
+
let maxAt = "";
|
|
2589
|
+
for (const s of series) {
|
|
2590
|
+
s.values.forEach((v, i) => {
|
|
2591
|
+
if (v < min) {
|
|
2592
|
+
min = v;
|
|
2593
|
+
minAt = labels[i] ?? String(i + 1);
|
|
2594
|
+
}
|
|
2595
|
+
if (v > max) {
|
|
2596
|
+
max = v;
|
|
2597
|
+
maxAt = labels[i] ?? String(i + 1);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
2602
|
+
parts.push(t("charts.a11y.min", { p0: fmt2(min), p1: minAt }));
|
|
2603
|
+
parts.push(t("charts.a11y.max", { p0: fmt2(max), p1: maxAt }));
|
|
2604
|
+
}
|
|
2605
|
+
const first = series[0]?.values ?? [];
|
|
2606
|
+
if (first.length > 1) {
|
|
2607
|
+
const d = first[first.length - 1] - first[0];
|
|
2608
|
+
const key = Math.abs(d) < 1e-9 ? "charts.a11y.trendFlat" : d > 0 ? "charts.a11y.trendUp" : "charts.a11y.trendDown";
|
|
2609
|
+
parts.push(t("charts.a11y.trend", { p0: t(key) }));
|
|
2610
|
+
}
|
|
2611
|
+
return parts.join("\uFF1B");
|
|
2612
|
+
}
|
|
2613
|
+
function buildChartTable(view, spec, type, fmt2) {
|
|
2614
|
+
const table = document.createElement("table");
|
|
2615
|
+
table.className = "icen-chart-table";
|
|
2616
|
+
const caption = document.createElement("caption");
|
|
2617
|
+
caption.textContent = t("charts.a11y.tableCaption", { p0: spec.title ?? String(type) });
|
|
2618
|
+
table.appendChild(caption);
|
|
2619
|
+
const addRow = (target, cells, head) => {
|
|
2620
|
+
const tr = document.createElement("tr");
|
|
2621
|
+
for (const c of cells) {
|
|
2622
|
+
const cell = document.createElement(head ? "th" : "td");
|
|
2623
|
+
if (head) cell.scope = target.tagName === "THEAD" ? "col" : "row";
|
|
2624
|
+
cell.textContent = c;
|
|
2625
|
+
tr.appendChild(cell);
|
|
2626
|
+
}
|
|
2627
|
+
target.appendChild(tr);
|
|
2628
|
+
};
|
|
2629
|
+
const thead = table.createTHead();
|
|
2630
|
+
const tbody = table.createTBody();
|
|
2631
|
+
if (type === "scatter") {
|
|
2632
|
+
addRow(thead, ["x", "y"], true);
|
|
2633
|
+
for (const p of view.points ?? []) addRow(tbody, [fmt2(p.x), fmt2(p.y)], false);
|
|
2634
|
+
return table;
|
|
2635
|
+
}
|
|
2636
|
+
const series = view.series && view.series.length > 0 ? view.series : [{ name: "", values: view.values ?? [] }];
|
|
2637
|
+
const labels = view.labels ?? [];
|
|
2638
|
+
addRow(thead, ["", ...series.map((s, i) => s.name || String(i + 1))], true);
|
|
2639
|
+
const n = Math.max(labels.length, ...series.map((s) => s.values.length));
|
|
2640
|
+
for (let i = 0; i < n; i++) {
|
|
2641
|
+
addRow(tbody, [labels[i] ?? String(i + 1), ...series.map((s) => fmt2(s.values[i] ?? 0))], false);
|
|
2642
|
+
}
|
|
2643
|
+
return table;
|
|
2644
|
+
}
|
|
2534
2645
|
function svgDimsOf(svg) {
|
|
2535
2646
|
const vb = (svg.getAttribute("viewBox") ?? "").split(/\s+/).map(Number);
|
|
2536
2647
|
const w = Number.isFinite(vb[2]) && (vb[2] ?? 0) > 0 ? vb[2] : CHART_LINE_W;
|
|
@@ -2871,6 +2982,7 @@ function renderChart(el, raw) {
|
|
|
2871
2982
|
if (!isZoomableType(t2) || fullLength() < 2) return;
|
|
2872
2983
|
const plot = t2 === "vbar" ? root.querySelector(".icen-chart-vbar") : root.querySelector(".icen-chart-line svg");
|
|
2873
2984
|
if (!plot) return;
|
|
2985
|
+
plot.style.touchAction = current2.zoom === "x" ? "pan-y" : "none";
|
|
2874
2986
|
const on = (type, fn) => {
|
|
2875
2987
|
plot.addEventListener(type, fn);
|
|
2876
2988
|
};
|
|
@@ -2926,6 +3038,149 @@ function renderChart(el, raw) {
|
|
|
2926
3038
|
render();
|
|
2927
3039
|
});
|
|
2928
3040
|
};
|
|
3041
|
+
const KB_TYPES = /* @__PURE__ */ new Set(["line", "area", "vbar", "hbar", "scatter"]);
|
|
3042
|
+
const kbCursor = { si: 0, i: 0, active: false };
|
|
3043
|
+
let liveRegion = null;
|
|
3044
|
+
let a11ySeq = 0;
|
|
3045
|
+
const a11yOf = () => ({
|
|
3046
|
+
summary: current2.a11y?.summary !== false,
|
|
3047
|
+
table: current2.a11y?.table === true,
|
|
3048
|
+
keyboard: current2.a11y?.keyboard !== false
|
|
3049
|
+
});
|
|
3050
|
+
const seriesViewOf = (view) => view.series && view.series.length > 0 ? view.series : [{ name: "", values: view.values ?? [] }];
|
|
3051
|
+
const kbPointCount = (view) => typeOf() === "scatter" ? view.points?.length ?? 0 : Math.max(0, ...seriesViewOf(view).map((s) => s.values.length), view.labels?.length ?? 0);
|
|
3052
|
+
const kbCursorEl = () => {
|
|
3053
|
+
const t2 = typeOf();
|
|
3054
|
+
const view = viewOf();
|
|
3055
|
+
const series = seriesViewOf(view);
|
|
3056
|
+
const n = kbPointCount(view);
|
|
3057
|
+
if (n < 1) return null;
|
|
3058
|
+
const si = clampNum(kbCursor.si, 0, Math.max(0, series.length - 1));
|
|
3059
|
+
const i = clampNum(kbCursor.i, 0, n - 1);
|
|
3060
|
+
if (t2 === "line" || t2 === "area") {
|
|
3061
|
+
const svg = root.querySelector(".icen-chart-line svg");
|
|
3062
|
+
if (!svg) return null;
|
|
3063
|
+
const dots = svg.querySelectorAll("circle");
|
|
3064
|
+
let base = 0;
|
|
3065
|
+
for (let k = 0; k < si; k++) base += Math.min(series[k].values.length, n);
|
|
3066
|
+
return dots.item(base + i) ?? null;
|
|
3067
|
+
}
|
|
3068
|
+
if (t2 === "scatter") return root.querySelector(".icen-chart-scatter svg")?.querySelectorAll("circle").item(i) ?? null;
|
|
3069
|
+
if (t2 === "vbar") return root.querySelectorAll(".icen-chart-vbar-bar").item(series.length > 1 ? i * series.length + si : i) ?? null;
|
|
3070
|
+
if (t2 === "hbar") return root.querySelectorAll(".icen-chart-hbar-bar").item(i) ?? null;
|
|
3071
|
+
return null;
|
|
3072
|
+
};
|
|
3073
|
+
const announceKb = (text) => {
|
|
3074
|
+
if (!liveRegion || !liveRegion.isConnected) {
|
|
3075
|
+
liveRegion = document.createElement("div");
|
|
3076
|
+
liveRegion.className = "icen-chart-live";
|
|
3077
|
+
liveRegion.setAttribute("aria-live", "polite");
|
|
3078
|
+
root.appendChild(liveRegion);
|
|
3079
|
+
}
|
|
3080
|
+
liveRegion.textContent = "";
|
|
3081
|
+
liveRegion.textContent = text;
|
|
3082
|
+
};
|
|
3083
|
+
const applyKbCursor = () => {
|
|
3084
|
+
for (const el2 of root.querySelectorAll(".icen-chart-kb-focus")) el2.classList.remove("icen-chart-kb-focus");
|
|
3085
|
+
if (!kbCursor.active) return;
|
|
3086
|
+
kbCursorEl()?.classList.add("icen-chart-kb-focus");
|
|
3087
|
+
};
|
|
3088
|
+
const announceCursor = () => {
|
|
3089
|
+
const view = viewOf();
|
|
3090
|
+
const series = seriesViewOf(view);
|
|
3091
|
+
const n = kbPointCount(view);
|
|
3092
|
+
if (n < 1) return;
|
|
3093
|
+
const si = clampNum(kbCursor.si, 0, Math.max(0, series.length - 1));
|
|
3094
|
+
const i = clampNum(kbCursor.i, 0, Math.max(0, n - 1));
|
|
3095
|
+
const fmt2 = chartFormatValue(current2.format);
|
|
3096
|
+
if (typeOf() === "scatter") {
|
|
3097
|
+
const p = view.points?.[i];
|
|
3098
|
+
if (!p) return;
|
|
3099
|
+
announceKb(t("charts.a11y.point", { p0: `x ${fmt2(p.x)}`, p1: `y ${fmt2(p.y)}`, p2: i + 1, p3: n }));
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
const label = view.labels?.[i] ?? String(i + 1);
|
|
3103
|
+
const value = series[si]?.values[i] ?? 0;
|
|
3104
|
+
if (series.length > 1) announceKb(t("charts.a11y.pointSeries", { p0: series[si].name || String(si + 1), p1: label, p2: fmt2(value), p3: i + 1, p4: n }));
|
|
3105
|
+
else announceKb(t("charts.a11y.point", { p0: label, p1: fmt2(value), p2: i + 1, p3: n }));
|
|
3106
|
+
};
|
|
3107
|
+
const onKbKeydown = (e) => {
|
|
3108
|
+
const conf = a11yOf();
|
|
3109
|
+
if (!conf.keyboard || !KB_TYPES.has(typeOf())) return;
|
|
3110
|
+
const g = root.querySelector("[data-icen-chart-kb]");
|
|
3111
|
+
if (!g || e.target !== g) return;
|
|
3112
|
+
if (e.key === "Escape") {
|
|
3113
|
+
if (zoomWindow) {
|
|
3114
|
+
zoomWindow = null;
|
|
3115
|
+
kbCursor.active = false;
|
|
3116
|
+
render();
|
|
3117
|
+
}
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
const view = viewOf();
|
|
3121
|
+
const series = seriesViewOf(view);
|
|
3122
|
+
const n = kbPointCount(view);
|
|
3123
|
+
if (n < 1) return;
|
|
3124
|
+
const siMax = Math.max(0, series.length - 1);
|
|
3125
|
+
let moved = true;
|
|
3126
|
+
switch (e.key) {
|
|
3127
|
+
case "ArrowRight":
|
|
3128
|
+
kbCursor.i = clampNum(kbCursor.i + 1, 0, n - 1);
|
|
3129
|
+
break;
|
|
3130
|
+
case "ArrowLeft":
|
|
3131
|
+
kbCursor.i = clampNum(kbCursor.i - 1, 0, n - 1);
|
|
3132
|
+
break;
|
|
3133
|
+
case "ArrowDown":
|
|
3134
|
+
kbCursor.si = clampNum(kbCursor.si + 1, 0, siMax);
|
|
3135
|
+
break;
|
|
3136
|
+
case "ArrowUp":
|
|
3137
|
+
kbCursor.si = clampNum(kbCursor.si - 1, 0, siMax);
|
|
3138
|
+
break;
|
|
3139
|
+
case "Home":
|
|
3140
|
+
kbCursor.i = 0;
|
|
3141
|
+
break;
|
|
3142
|
+
case "End":
|
|
3143
|
+
kbCursor.i = n - 1;
|
|
3144
|
+
break;
|
|
3145
|
+
default:
|
|
3146
|
+
moved = false;
|
|
3147
|
+
}
|
|
3148
|
+
if (!moved) return;
|
|
3149
|
+
e.preventDefault();
|
|
3150
|
+
kbCursor.active = true;
|
|
3151
|
+
applyKbCursor();
|
|
3152
|
+
announceCursor();
|
|
3153
|
+
};
|
|
3154
|
+
root.addEventListener("keydown", onKbKeydown);
|
|
3155
|
+
const applyChartA11yPlus = (view, spec, type, formatValue) => {
|
|
3156
|
+
const conf = a11yOf();
|
|
3157
|
+
const graphic = root.querySelector('[role="img"]');
|
|
3158
|
+
if (!graphic) return;
|
|
3159
|
+
const body = root.querySelector(".icen-chart-body") ?? graphic.parentElement ?? root;
|
|
3160
|
+
const summaryParts = [];
|
|
3161
|
+
if (conf.summary) summaryParts.push(summarizeChart(view, type, formatValue));
|
|
3162
|
+
if (conf.keyboard && KB_TYPES.has(type)) summaryParts.push(t("charts.a11y.keyboardHint"));
|
|
3163
|
+
const summaryText = summaryParts.filter(Boolean).join("\uFF1B");
|
|
3164
|
+
if (summaryText) {
|
|
3165
|
+
const p = document.createElement("p");
|
|
3166
|
+
p.className = "icen-chart-summary";
|
|
3167
|
+
p.id = `icen-chart-a11y-${++a11ySeq}`;
|
|
3168
|
+
p.textContent = summaryText;
|
|
3169
|
+
body.appendChild(p);
|
|
3170
|
+
graphic.setAttribute("aria-describedby", p.id);
|
|
3171
|
+
} else {
|
|
3172
|
+
graphic.removeAttribute("aria-describedby");
|
|
3173
|
+
}
|
|
3174
|
+
if (conf.keyboard && KB_TYPES.has(type)) {
|
|
3175
|
+
graphic.setAttribute("tabindex", "0");
|
|
3176
|
+
graphic.setAttribute("data-icen-chart-kb", "");
|
|
3177
|
+
} else {
|
|
3178
|
+
graphic.removeAttribute("tabindex");
|
|
3179
|
+
graphic.removeAttribute("data-icen-chart-kb");
|
|
3180
|
+
}
|
|
3181
|
+
if (conf.table) body.appendChild(buildChartTable(view, spec, type, formatValue));
|
|
3182
|
+
applyKbCursor();
|
|
3183
|
+
};
|
|
2929
3184
|
const render = () => {
|
|
2930
3185
|
if (!current2.type) current2 = { ...current2, type: inferChartType(current2) };
|
|
2931
3186
|
const spec = current2;
|
|
@@ -2997,9 +3252,16 @@ function renderChart(el, raw) {
|
|
|
2997
3252
|
const graphic = root.querySelector('[role="img"]');
|
|
2998
3253
|
if (graphic && !graphic.getAttribute("aria-label")) {
|
|
2999
3254
|
graphic.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
|
|
3255
|
+
} else if (!graphic) {
|
|
3256
|
+
const dom = root.querySelector(".icen-chart-vbar, .icen-chart-hbar");
|
|
3257
|
+
if (dom) {
|
|
3258
|
+
dom.setAttribute("role", "img");
|
|
3259
|
+
dom.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
|
|
3260
|
+
}
|
|
3000
3261
|
}
|
|
3001
3262
|
}
|
|
3002
3263
|
if (loading) return;
|
|
3264
|
+
applyChartA11yPlus(view, spec, type, formatValue);
|
|
3003
3265
|
paintChartMarks(root, view, spec, formatValue, warnedMarks);
|
|
3004
3266
|
bindZoomGesture();
|
|
3005
3267
|
};
|
|
@@ -3081,6 +3343,7 @@ function renderChart(el, raw) {
|
|
|
3081
3343
|
releaseFluidWidth(root);
|
|
3082
3344
|
hiddenLegendKeys.delete(root);
|
|
3083
3345
|
zoomDragCleanup?.();
|
|
3346
|
+
root.removeEventListener("keydown", onKbKeydown);
|
|
3084
3347
|
root.textContent = "";
|
|
3085
3348
|
root.classList.remove("is-loading");
|
|
3086
3349
|
unbindEvents();
|
|
@@ -3498,7 +3761,13 @@ function onKeyDown(e) {
|
|
|
3498
3761
|
|
|
3499
3762
|
// src/behaviors/datatable.ts
|
|
3500
3763
|
function defaultCompare(a, b) {
|
|
3501
|
-
if (typeof a === "number" && typeof b === "number")
|
|
3764
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
3765
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
3766
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return 0;
|
|
3767
|
+
return Number.isNaN(a) ? 1 : -1;
|
|
3768
|
+
}
|
|
3769
|
+
return a - b;
|
|
3770
|
+
}
|
|
3502
3771
|
return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
|
|
3503
3772
|
}
|
|
3504
3773
|
function toNum2(v) {
|
|
@@ -3787,11 +4056,13 @@ function createTable(el, opts) {
|
|
|
3787
4056
|
const body = document.createElement("div");
|
|
3788
4057
|
body.className = "icen-dt-body";
|
|
3789
4058
|
body.setAttribute("role", "rowgroup");
|
|
3790
|
-
const summaryEl = document.createElement("div");
|
|
3791
|
-
summaryEl
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
4059
|
+
const summaryEl = opts.summaryRow ? document.createElement("div") : null;
|
|
4060
|
+
if (summaryEl) {
|
|
4061
|
+
summaryEl.className = "icen-dt-row icen-dt-row--summary";
|
|
4062
|
+
summaryEl.setAttribute("role", "row");
|
|
4063
|
+
summaryEl.hidden = true;
|
|
4064
|
+
}
|
|
4065
|
+
canvas.append(head, body, ...summaryEl ? [summaryEl] : []);
|
|
3795
4066
|
scroll.appendChild(canvas);
|
|
3796
4067
|
root.appendChild(scroll);
|
|
3797
4068
|
const foot = document.createElement("div");
|
|
@@ -4604,7 +4875,8 @@ function createTable(el, opts) {
|
|
|
4604
4875
|
else wrap.textContent = String(out);
|
|
4605
4876
|
return wrap;
|
|
4606
4877
|
}
|
|
4607
|
-
function
|
|
4878
|
+
function renderSummary2() {
|
|
4879
|
+
if (!summaryEl) return;
|
|
4608
4880
|
summaryEl.textContent = "";
|
|
4609
4881
|
summaryEl.hidden = true;
|
|
4610
4882
|
const cfg = opts.summaryRow;
|
|
@@ -4801,9 +5073,9 @@ function createTable(el, opts) {
|
|
|
4801
5073
|
}
|
|
4802
5074
|
}
|
|
4803
5075
|
let currentTotal = 0;
|
|
4804
|
-
function visibleRange(total) {
|
|
5076
|
+
function visibleRange(total, avail) {
|
|
4805
5077
|
if (!virtual) {
|
|
4806
|
-
if (opts.remote || opts.pagination === false) return [0, total];
|
|
5078
|
+
if (opts.remote || opts.pagination === false) return [0, Math.min(total, avail)];
|
|
4807
5079
|
const start2 = (page - 1) * pageSize;
|
|
4808
5080
|
return [start2, Math.min(total, start2 + pageSize)];
|
|
4809
5081
|
}
|
|
@@ -4818,6 +5090,10 @@ function createTable(el, opts) {
|
|
|
4818
5090
|
const rows = pipeline();
|
|
4819
5091
|
const entries = groupFn ? flatEntries() : null;
|
|
4820
5092
|
const total = entries ? entries.length : opts.remote ? opts.totalCount ?? data.length : rows.length;
|
|
5093
|
+
if (!opts.remote && opts.pagination !== false) {
|
|
5094
|
+
const pages = Math.max(1, Math.ceil(total / pageSize));
|
|
5095
|
+
if (page > pages) page = pages;
|
|
5096
|
+
}
|
|
4821
5097
|
navTotal = entries ? entries.length : rows.length;
|
|
4822
5098
|
currentTotal = total;
|
|
4823
5099
|
const keepTop = virtual ? scroll.scrollTop : 0;
|
|
@@ -4834,7 +5110,7 @@ function createTable(el, opts) {
|
|
|
4834
5110
|
body.appendChild(sk);
|
|
4835
5111
|
}
|
|
4836
5112
|
renderFoot(total, rows.length);
|
|
4837
|
-
|
|
5113
|
+
renderSummary2();
|
|
4838
5114
|
return;
|
|
4839
5115
|
}
|
|
4840
5116
|
if (total === 0) {
|
|
@@ -4843,10 +5119,10 @@ function createTable(el, opts) {
|
|
|
4843
5119
|
empty.textContent = opts.emptyLabel ?? t("datatable.empty");
|
|
4844
5120
|
body.appendChild(empty);
|
|
4845
5121
|
renderFoot(total, rows.length);
|
|
4846
|
-
|
|
5122
|
+
renderSummary2();
|
|
4847
5123
|
return;
|
|
4848
5124
|
}
|
|
4849
|
-
const [start, end] = visibleRange(total);
|
|
5125
|
+
const [start, end] = visibleRange(total, rows.length);
|
|
4850
5126
|
const frag = document.createDocumentFragment();
|
|
4851
5127
|
const offsets = frozenOffsets();
|
|
4852
5128
|
if (virtual && start > 0) {
|
|
@@ -4880,7 +5156,7 @@ function createTable(el, opts) {
|
|
|
4880
5156
|
if (virtual) scroll.scrollTop = keepTop;
|
|
4881
5157
|
syncNavFocus();
|
|
4882
5158
|
renderFoot(total, rows.length);
|
|
4883
|
-
|
|
5159
|
+
renderSummary2();
|
|
4884
5160
|
}
|
|
4885
5161
|
function renderFoot(total, dataCount = total) {
|
|
4886
5162
|
foot.textContent = "";
|
|
@@ -4909,11 +5185,21 @@ function createTable(el, opts) {
|
|
|
4909
5185
|
const pages = Math.ceil(total / pageSize);
|
|
4910
5186
|
const pager = document.createElement("nav");
|
|
4911
5187
|
pager.className = "icen-dt-pager";
|
|
4912
|
-
|
|
5188
|
+
{
|
|
5189
|
+
const base = t("datatable.paginationAria");
|
|
5190
|
+
const used = /* @__PURE__ */ new Set();
|
|
5191
|
+
for (const el2 of document.querySelectorAll(".icen-dt-pager[aria-label]")) {
|
|
5192
|
+
if (!root.contains(el2)) used.add(el2.getAttribute("aria-label") ?? "");
|
|
5193
|
+
}
|
|
5194
|
+
let label = base, n = 2;
|
|
5195
|
+
while (used.has(label)) label = `${base} ${n++}`;
|
|
5196
|
+
pager.setAttribute("aria-label", label);
|
|
5197
|
+
}
|
|
4913
5198
|
const sizeSel = document.createElement("select");
|
|
4914
5199
|
sizeSel.className = "icen-dt-page-size";
|
|
4915
5200
|
sizeSel.setAttribute("aria-label", t("datatable.pageSizeAria"));
|
|
4916
|
-
|
|
5201
|
+
const sizeOptions = pageSizes.includes(pageSize) ? pageSizes : [...pageSizes, pageSize].sort((a, b) => a - b);
|
|
5202
|
+
for (const s of sizeOptions) {
|
|
4917
5203
|
const o = document.createElement("option");
|
|
4918
5204
|
o.value = String(s);
|
|
4919
5205
|
o.textContent = t("pagination.pageSize", { p0: s });
|
|
@@ -5005,6 +5291,7 @@ function createTable(el, opts) {
|
|
|
5005
5291
|
if (destroyed) return;
|
|
5006
5292
|
renderHead();
|
|
5007
5293
|
renderBody();
|
|
5294
|
+
syncFrozenWidths();
|
|
5008
5295
|
}
|
|
5009
5296
|
rekey();
|
|
5010
5297
|
renderAll();
|
|
@@ -5025,12 +5312,52 @@ function createTable(el, opts) {
|
|
|
5025
5312
|
opts.onPageChange?.(page, pageSize);
|
|
5026
5313
|
renderAll();
|
|
5027
5314
|
}
|
|
5315
|
+
function hasFluidFrozen() {
|
|
5316
|
+
return cols().some((c) => frozenSide(c) != null && !/^(\d+(?:\.\d+)?)px/.test(colWidths.get(c.key) ?? c.width ?? ""));
|
|
5317
|
+
}
|
|
5318
|
+
function syncFrozenWidths() {
|
|
5319
|
+
if (!hasFluidFrozen()) return;
|
|
5320
|
+
const visible = cols();
|
|
5321
|
+
const headCells = Array.from(headRow.children);
|
|
5322
|
+
const lead = headCells.length - visible.length;
|
|
5323
|
+
const left = /* @__PURE__ */ new Map();
|
|
5324
|
+
const right = /* @__PURE__ */ new Map();
|
|
5325
|
+
let acc = headCells.slice(0, lead).reduce((s, c) => s + c.offsetWidth, 0);
|
|
5326
|
+
for (let i = 0; i < visible.length; i++) {
|
|
5327
|
+
const c = visible[i];
|
|
5328
|
+
if (frozenSide(c) === "left") left.set(c.key, acc);
|
|
5329
|
+
acc += headCells[lead + i]?.offsetWidth ?? 0;
|
|
5330
|
+
}
|
|
5331
|
+
let accR = 0;
|
|
5332
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
5333
|
+
const c = visible[i];
|
|
5334
|
+
if (frozenSide(c) === "right") right.set(c.key, accR);
|
|
5335
|
+
accR += headCells[lead + i]?.offsetWidth ?? 0;
|
|
5336
|
+
}
|
|
5337
|
+
const rows = [headRow, ...summaryEl ? [summaryEl] : [], ...Array.from(body.querySelectorAll(".icen-dt-row"))];
|
|
5338
|
+
for (const row of rows) {
|
|
5339
|
+
const cells = Array.from(row.children);
|
|
5340
|
+
if (cells.length !== headCells.length) continue;
|
|
5341
|
+
cells.forEach((cell, i) => {
|
|
5342
|
+
const col = i >= lead ? visible[i - lead] : void 0;
|
|
5343
|
+
if (!col) return;
|
|
5344
|
+
const l = left.get(col.key);
|
|
5345
|
+
if (l != null && cell.classList.contains("icen-dt-cell--frozen")) cell.style.left = `${l}px`;
|
|
5346
|
+
const r = right.get(col.key);
|
|
5347
|
+
if (r != null && cell.classList.contains("icen-dt-cell--frozen-right")) cell.style.right = `${r}px`;
|
|
5348
|
+
});
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5028
5351
|
return {
|
|
5029
5352
|
setPage,
|
|
5030
5353
|
setPageSize,
|
|
5031
5354
|
setData(next) {
|
|
5032
5355
|
if (editing) cancelEdit(true);
|
|
5033
5356
|
data = next;
|
|
5357
|
+
if (typeof opts.rowKey !== "function" && typeof opts.rowKey !== "string" && selected.size > 0) {
|
|
5358
|
+
selected.clear();
|
|
5359
|
+
emitSelection();
|
|
5360
|
+
}
|
|
5034
5361
|
rekey();
|
|
5035
5362
|
invalidate();
|
|
5036
5363
|
page = 1;
|
|
@@ -5107,6 +5434,823 @@ function createTable(el, opts) {
|
|
|
5107
5434
|
};
|
|
5108
5435
|
}
|
|
5109
5436
|
|
|
5437
|
+
// src/behaviors/form-validate.ts
|
|
5438
|
+
var DEFAULT_MESSAGES = {
|
|
5439
|
+
required: t("formValidate.required"),
|
|
5440
|
+
email: t("formValidate.email"),
|
|
5441
|
+
url: t("formValidate.url"),
|
|
5442
|
+
number: t("formValidate.number"),
|
|
5443
|
+
minlength: t("formValidate.minlength"),
|
|
5444
|
+
maxlength: t("formValidate.maxlength"),
|
|
5445
|
+
pattern: t("formValidate.pattern"),
|
|
5446
|
+
min: t("formValidate.min"),
|
|
5447
|
+
max: t("formValidate.max"),
|
|
5448
|
+
step: t("formValidate.step"),
|
|
5449
|
+
matches: t("formValidate.matches")
|
|
5450
|
+
};
|
|
5451
|
+
var messageTable = { ...DEFAULT_MESSAGES };
|
|
5452
|
+
function formatMessage(template, tokens) {
|
|
5453
|
+
return template.replace(/\{(\w+)\}/g, (m, key) => key in tokens ? String(tokens[key]) : m);
|
|
5454
|
+
}
|
|
5455
|
+
var customValidators = /* @__PURE__ */ new Map();
|
|
5456
|
+
function attrNumber(el, name) {
|
|
5457
|
+
const raw = el.getAttribute(name);
|
|
5458
|
+
if (raw == null || raw === "" || !Number.isFinite(Number(raw))) return null;
|
|
5459
|
+
return Number(raw);
|
|
5460
|
+
}
|
|
5461
|
+
function resolveFieldRules(field) {
|
|
5462
|
+
const rules = [];
|
|
5463
|
+
const type = (field.getAttribute("type") ?? "").toLowerCase();
|
|
5464
|
+
if (field.hasAttribute("required")) rules.push({ rule: "required" });
|
|
5465
|
+
if (type === "email" || type === "url") rules.push({ rule: type });
|
|
5466
|
+
if (type === "number" || type === "range") rules.push({ rule: "number" });
|
|
5467
|
+
const minlength = attrNumber(field, "minlength");
|
|
5468
|
+
if (minlength != null) rules.push({ rule: "minlength", attr: String(minlength) });
|
|
5469
|
+
const maxlength = attrNumber(field, "maxlength");
|
|
5470
|
+
if (maxlength != null) rules.push({ rule: "maxlength", attr: String(maxlength) });
|
|
5471
|
+
const pattern = field.getAttribute("pattern");
|
|
5472
|
+
if (pattern) rules.push({ rule: "pattern", attr: pattern });
|
|
5473
|
+
const min = field.getAttribute("min");
|
|
5474
|
+
if (min != null && min !== "") rules.push({ rule: "min", attr: min });
|
|
5475
|
+
const max = field.getAttribute("max");
|
|
5476
|
+
if (max != null && max !== "") rules.push({ rule: "max", attr: max });
|
|
5477
|
+
const step = field.getAttribute("step");
|
|
5478
|
+
if (step != null && step !== "" && step !== "any") rules.push({ rule: "step", attr: step });
|
|
5479
|
+
const extra = field.getAttribute("data-icen-validate") ?? "";
|
|
5480
|
+
for (const token of extra.trim().split(/\s+/)) {
|
|
5481
|
+
if (!token) continue;
|
|
5482
|
+
const idx = token.indexOf(":");
|
|
5483
|
+
const rule = idx === -1 ? token : token.slice(0, idx);
|
|
5484
|
+
const arg = idx === -1 ? void 0 : token.slice(idx + 1) || void 0;
|
|
5485
|
+
if (!rule || rule === "matches" && !arg) continue;
|
|
5486
|
+
rules.push({ rule, arg });
|
|
5487
|
+
}
|
|
5488
|
+
return rules;
|
|
5489
|
+
}
|
|
5490
|
+
function fieldValue(field) {
|
|
5491
|
+
if (field instanceof HTMLInputElement && (field.type === "checkbox" || field.type === "radio")) {
|
|
5492
|
+
return field.checked ? field.value || "on" : "";
|
|
5493
|
+
}
|
|
5494
|
+
return field.value;
|
|
5495
|
+
}
|
|
5496
|
+
function fieldLabel(field, fallback = "") {
|
|
5497
|
+
if (!field) return fallback;
|
|
5498
|
+
const wrap = field.closest(".icen-field, .icen-form-field");
|
|
5499
|
+
const labelEl = wrap?.querySelector(".icen-field-label, .icen-form-label, label");
|
|
5500
|
+
const text = labelEl?.textContent?.replace(/\s+/g, " ").trim();
|
|
5501
|
+
return text || field.getAttribute("aria-label") || field.getAttribute("placeholder") || field.getAttribute("name") || fallback;
|
|
5502
|
+
}
|
|
5503
|
+
function ruleMessage(field, rule, tokens) {
|
|
5504
|
+
const custom2 = field?.getAttribute(`data-icen-msg-${rule}`);
|
|
5505
|
+
const template = custom2 || messageTable[rule] || DEFAULT_MESSAGES[rule] || t("formValidate.invalid");
|
|
5506
|
+
return formatMessage(template, tokens);
|
|
5507
|
+
}
|
|
5508
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5509
|
+
var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "range"]);
|
|
5510
|
+
function compareValue(field, value, attr) {
|
|
5511
|
+
const numericAttr = Number.isFinite(Number(attr));
|
|
5512
|
+
const type = field?.getAttribute("type")?.toLowerCase() ?? "";
|
|
5513
|
+
if (numericAttr && (NUMBER_TYPES.has(type) || Number.isFinite(Number(value)))) {
|
|
5514
|
+
if (!Number.isFinite(Number(value))) return null;
|
|
5515
|
+
return Number(value) - Number(attr);
|
|
5516
|
+
}
|
|
5517
|
+
if (!numericAttr) return value < attr ? -1 : value > attr ? 1 : 0;
|
|
5518
|
+
return null;
|
|
5519
|
+
}
|
|
5520
|
+
async function applyRule(value, rule, ctx) {
|
|
5521
|
+
const field = ctx.field;
|
|
5522
|
+
const tokens = {
|
|
5523
|
+
min: rule.attr ?? "",
|
|
5524
|
+
max: rule.attr ?? "",
|
|
5525
|
+
step: rule.attr ?? "",
|
|
5526
|
+
length: value.length,
|
|
5527
|
+
label: fieldLabel(field, rule.arg ?? rule.rule)
|
|
5528
|
+
};
|
|
5529
|
+
switch (rule.rule) {
|
|
5530
|
+
case "required": {
|
|
5531
|
+
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
5532
|
+
return field.checked ? true : ruleMessage(field, "required", tokens);
|
|
5533
|
+
}
|
|
5534
|
+
if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
5535
|
+
const form = ctx.form ?? field.form;
|
|
5536
|
+
const checked = form ? Array.from(form.elements).some(
|
|
5537
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === field.name && el.checked
|
|
5538
|
+
) : field.checked;
|
|
5539
|
+
return checked ? true : ruleMessage(field, "required", tokens);
|
|
5540
|
+
}
|
|
5541
|
+
return value.trim() !== "" ? true : ruleMessage(field, "required", tokens);
|
|
5542
|
+
}
|
|
5543
|
+
case "email":
|
|
5544
|
+
return value === "" || EMAIL_RE.test(value) ? true : ruleMessage(field, "email", tokens);
|
|
5545
|
+
case "url": {
|
|
5546
|
+
if (value === "") return true;
|
|
5547
|
+
try {
|
|
5548
|
+
new URL(value);
|
|
5549
|
+
return true;
|
|
5550
|
+
} catch {
|
|
5551
|
+
return ruleMessage(field, "url", tokens);
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
case "number":
|
|
5555
|
+
return value === "" || Number.isFinite(Number(value)) ? true : ruleMessage(field, "number", tokens);
|
|
5556
|
+
case "minlength": {
|
|
5557
|
+
if (value === "") return true;
|
|
5558
|
+
const min = Number(rule.attr);
|
|
5559
|
+
return value.length >= min ? true : ruleMessage(field, "minlength", tokens);
|
|
5560
|
+
}
|
|
5561
|
+
case "maxlength": {
|
|
5562
|
+
if (value === "") return true;
|
|
5563
|
+
const max = Number(rule.attr);
|
|
5564
|
+
return value.length <= max ? true : ruleMessage(field, "maxlength", tokens);
|
|
5565
|
+
}
|
|
5566
|
+
case "pattern": {
|
|
5567
|
+
if (value === "") return true;
|
|
5568
|
+
try {
|
|
5569
|
+
return new RegExp(`^(?:${rule.attr})$`).test(value) ? true : ruleMessage(field, "pattern", tokens);
|
|
5570
|
+
} catch {
|
|
5571
|
+
return true;
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
case "min": {
|
|
5575
|
+
if (value === "") return true;
|
|
5576
|
+
const cmp = compareValue(field, value, rule.attr ?? "");
|
|
5577
|
+
return cmp == null || cmp >= 0 ? true : ruleMessage(field, "min", tokens);
|
|
5578
|
+
}
|
|
5579
|
+
case "max": {
|
|
5580
|
+
if (value === "") return true;
|
|
5581
|
+
const cmp = compareValue(field, value, rule.attr ?? "");
|
|
5582
|
+
return cmp == null || cmp <= 0 ? true : ruleMessage(field, "max", tokens);
|
|
5583
|
+
}
|
|
5584
|
+
case "step": {
|
|
5585
|
+
if (value === "") return true;
|
|
5586
|
+
const step = Number(rule.attr);
|
|
5587
|
+
if (!Number.isFinite(step) || step <= 0 || !Number.isFinite(Number(value))) return true;
|
|
5588
|
+
const minAttr = field?.getAttribute("min");
|
|
5589
|
+
const base = minAttr != null && minAttr !== "" && Number.isFinite(Number(minAttr)) ? Number(minAttr) : 0;
|
|
5590
|
+
const rem = Math.abs((Number(value) - base) % step);
|
|
5591
|
+
return rem < 1e-9 || step - rem < 1e-9 ? true : ruleMessage(field, "step", tokens);
|
|
5592
|
+
}
|
|
5593
|
+
case "matches": {
|
|
5594
|
+
const form = ctx.form ?? field?.form ?? null;
|
|
5595
|
+
const other = form && rule.arg ? form.elements.namedItem(rule.arg) : null;
|
|
5596
|
+
const otherEl = other instanceof Element ? other : null;
|
|
5597
|
+
if (!otherEl) return true;
|
|
5598
|
+
return value === otherEl.value ? true : ruleMessage(field, "matches", tokens);
|
|
5599
|
+
}
|
|
5600
|
+
default: {
|
|
5601
|
+
const fn = customValidators.get(rule.rule);
|
|
5602
|
+
if (!fn || !field) return true;
|
|
5603
|
+
const result = await fn(value, field, ctx.form ?? field.form ?? null);
|
|
5604
|
+
return result === true ? true : typeof result === "string" ? result : true;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
async function evaluateRules(value, rules, ctx = {}) {
|
|
5609
|
+
for (const rule of rules) {
|
|
5610
|
+
const fail = await applyRule(value, rule, ctx);
|
|
5611
|
+
if (fail !== true) return fail;
|
|
5612
|
+
}
|
|
5613
|
+
return true;
|
|
5614
|
+
}
|
|
5615
|
+
var DEFAULT_DEBOUNCE = 160;
|
|
5616
|
+
var uidSeq = 0;
|
|
5617
|
+
var liveValidators = /* @__PURE__ */ new WeakMap();
|
|
5618
|
+
var composingFlags = /* @__PURE__ */ new WeakMap();
|
|
5619
|
+
var compositionTracked = /* @__PURE__ */ new WeakSet();
|
|
5620
|
+
function trackComposition(el) {
|
|
5621
|
+
if (compositionTracked.has(el)) return;
|
|
5622
|
+
compositionTracked.add(el);
|
|
5623
|
+
el.addEventListener("compositionstart", () => composingFlags.set(el, true));
|
|
5624
|
+
el.addEventListener("compositionend", () => composingFlags.set(el, false));
|
|
5625
|
+
}
|
|
5626
|
+
function isComposing(el) {
|
|
5627
|
+
return composingFlags.get(el) === true;
|
|
5628
|
+
}
|
|
5629
|
+
function collectFormFields(form) {
|
|
5630
|
+
const out = [];
|
|
5631
|
+
for (const el of Array.from(form.elements)) {
|
|
5632
|
+
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
|
|
5633
|
+
continue;
|
|
5634
|
+
}
|
|
5635
|
+
if (el.disabled || !el.name) continue;
|
|
5636
|
+
if (el instanceof HTMLInputElement) {
|
|
5637
|
+
const type = (el.getAttribute("type") ?? "").toLowerCase();
|
|
5638
|
+
if (["submit", "button", "reset", "image", "file"].includes(type)) continue;
|
|
5639
|
+
}
|
|
5640
|
+
out.push(el);
|
|
5641
|
+
}
|
|
5642
|
+
return out;
|
|
5643
|
+
}
|
|
5644
|
+
function findFormField(form, name) {
|
|
5645
|
+
const item = form.elements.namedItem(name);
|
|
5646
|
+
if (item instanceof Element && (item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement || item instanceof HTMLSelectElement)) {
|
|
5647
|
+
return item;
|
|
5648
|
+
}
|
|
5649
|
+
if (item instanceof RadioNodeList) {
|
|
5650
|
+
for (const el of Array.from(item)) {
|
|
5651
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
5652
|
+
return el;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
return null;
|
|
5657
|
+
}
|
|
5658
|
+
function fieldWrap(field) {
|
|
5659
|
+
return closestSelfOrAncestor(field, ".icen-field, .icen-form-field");
|
|
5660
|
+
}
|
|
5661
|
+
function closestSelfOrAncestor(el, selector) {
|
|
5662
|
+
return el.matches(selector) ? el : el.closest(selector);
|
|
5663
|
+
}
|
|
5664
|
+
function ensureControlId(field) {
|
|
5665
|
+
if (!field.id) field.id = `icen-fv-${++uidSeq}`;
|
|
5666
|
+
return field.id;
|
|
5667
|
+
}
|
|
5668
|
+
function findErrorEl(wrap) {
|
|
5669
|
+
for (const el of Array.from(wrap.children)) {
|
|
5670
|
+
if (el instanceof HTMLElement && el.classList.contains("icen-field-error")) return el;
|
|
5671
|
+
}
|
|
5672
|
+
return null;
|
|
5673
|
+
}
|
|
5674
|
+
function setValidating(field, on) {
|
|
5675
|
+
const wrap = fieldWrap(field);
|
|
5676
|
+
if (wrap) wrap.classList.toggle("is-validating", on);
|
|
5677
|
+
if (on) field.setAttribute("aria-busy", "true");
|
|
5678
|
+
else field.removeAttribute("aria-busy");
|
|
5679
|
+
}
|
|
5680
|
+
function presentField(field, outcome, showValid) {
|
|
5681
|
+
const wrap = fieldWrap(field);
|
|
5682
|
+
setValidating(field, false);
|
|
5683
|
+
if (!wrap) {
|
|
5684
|
+
if (!outcome.ok) field.setAttribute("aria-invalid", "true");
|
|
5685
|
+
else field.removeAttribute("aria-invalid");
|
|
5686
|
+
return;
|
|
5687
|
+
}
|
|
5688
|
+
if (!outcome.ok) {
|
|
5689
|
+
wrap.classList.add("is-error");
|
|
5690
|
+
wrap.setAttribute("data-icen-invalid", "true");
|
|
5691
|
+
wrap.classList.remove("is-valid");
|
|
5692
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5693
|
+
let errEl2 = findErrorEl(wrap);
|
|
5694
|
+
if (!errEl2) {
|
|
5695
|
+
errEl2 = document.createElement("p");
|
|
5696
|
+
errEl2.className = "icen-field-error";
|
|
5697
|
+
errEl2.id = `icen-fv-err-${++uidSeq}`;
|
|
5698
|
+
errEl2.setAttribute("role", "alert");
|
|
5699
|
+
errEl2.setAttribute("data-icen-fv-auto", "");
|
|
5700
|
+
wrap.appendChild(errEl2);
|
|
5701
|
+
}
|
|
5702
|
+
errEl2.hidden = false;
|
|
5703
|
+
errEl2.textContent = outcome.message ?? "";
|
|
5704
|
+
field.setAttribute("aria-invalid", "true");
|
|
5705
|
+
const described = new Set((field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean));
|
|
5706
|
+
described.add(errEl2.id);
|
|
5707
|
+
field.setAttribute("aria-describedby", Array.from(described).join(" "));
|
|
5708
|
+
return;
|
|
5709
|
+
}
|
|
5710
|
+
wrap.classList.remove("is-error");
|
|
5711
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5712
|
+
field.removeAttribute("aria-invalid");
|
|
5713
|
+
const errEl = findErrorEl(wrap);
|
|
5714
|
+
if (errEl) {
|
|
5715
|
+
const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => id !== errEl.id);
|
|
5716
|
+
if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
|
|
5717
|
+
else field.removeAttribute("aria-describedby");
|
|
5718
|
+
if (errEl.hasAttribute("data-icen-fv-auto")) errEl.remove();
|
|
5719
|
+
else {
|
|
5720
|
+
errEl.textContent = "";
|
|
5721
|
+
errEl.hidden = true;
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
if (showValid) {
|
|
5725
|
+
wrap.classList.add("is-valid");
|
|
5726
|
+
wrap.setAttribute("data-icen-valid", "true");
|
|
5727
|
+
} else {
|
|
5728
|
+
wrap.classList.remove("is-valid");
|
|
5729
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
function clearFieldPresentation(field) {
|
|
5733
|
+
const wrap = fieldWrap(field);
|
|
5734
|
+
if (wrap) {
|
|
5735
|
+
wrap.classList.remove("is-error", "is-valid", "is-validating");
|
|
5736
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5737
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5738
|
+
const errEl = findErrorEl(wrap);
|
|
5739
|
+
if (errEl?.hasAttribute("data-icen-fv-auto")) errEl.remove();
|
|
5740
|
+
else if (errEl) {
|
|
5741
|
+
errEl.textContent = "";
|
|
5742
|
+
errEl.hidden = true;
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
field.removeAttribute("aria-invalid");
|
|
5746
|
+
field.removeAttribute("aria-busy");
|
|
5747
|
+
const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => !id.startsWith("icen-fv-err-"));
|
|
5748
|
+
if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
|
|
5749
|
+
else field.removeAttribute("aria-describedby");
|
|
5750
|
+
}
|
|
5751
|
+
function focusField(field) {
|
|
5752
|
+
try {
|
|
5753
|
+
field.focus();
|
|
5754
|
+
} catch {
|
|
5755
|
+
}
|
|
5756
|
+
if (typeof field.scrollIntoView === "function") {
|
|
5757
|
+
try {
|
|
5758
|
+
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5759
|
+
field.scrollIntoView({ block: "center", behavior: reduced ? "auto" : "smooth" });
|
|
5760
|
+
} catch {
|
|
5761
|
+
try {
|
|
5762
|
+
field.scrollIntoView();
|
|
5763
|
+
} catch {
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
function renderSummary(form, errorsObj) {
|
|
5769
|
+
if (!form.hasAttribute("data-icen-validate-summary")) return;
|
|
5770
|
+
const names = Object.keys(errorsObj);
|
|
5771
|
+
const existing = form.querySelector(":scope > .icen-form-validate-summary");
|
|
5772
|
+
if (names.length === 0) {
|
|
5773
|
+
existing?.remove();
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
let box = existing;
|
|
5777
|
+
if (!box) {
|
|
5778
|
+
box = document.createElement("div");
|
|
5779
|
+
box.className = "icen-form-validate-summary";
|
|
5780
|
+
box.setAttribute("role", "alert");
|
|
5781
|
+
box.setAttribute("data-icen-fv-auto", "");
|
|
5782
|
+
box.addEventListener("click", (e) => {
|
|
5783
|
+
const a = e.target instanceof Element ? closestSelfOrAncestor(e.target, "a[data-icen-fv-name]") : null;
|
|
5784
|
+
if (!(a instanceof HTMLAnchorElement)) return;
|
|
5785
|
+
e.preventDefault();
|
|
5786
|
+
const field = findFormField(form, a.getAttribute("data-icen-fv-name") ?? "");
|
|
5787
|
+
if (field) focusField(field);
|
|
5788
|
+
});
|
|
5789
|
+
form.prepend(box);
|
|
5790
|
+
}
|
|
5791
|
+
box.textContent = "";
|
|
5792
|
+
const title = document.createElement("p");
|
|
5793
|
+
title.className = "icen-form-validate-summary-title";
|
|
5794
|
+
title.textContent = t("formValidate.summary", { p0: names.length });
|
|
5795
|
+
const list = document.createElement("ul");
|
|
5796
|
+
list.className = "icen-form-validate-summary-list";
|
|
5797
|
+
for (const name of names) {
|
|
5798
|
+
const field = findFormField(form, name);
|
|
5799
|
+
if (!field) continue;
|
|
5800
|
+
const li = document.createElement("li");
|
|
5801
|
+
const a = document.createElement("a");
|
|
5802
|
+
a.className = "icen-form-validate-summary-item";
|
|
5803
|
+
a.href = `#${ensureControlId(field)}`;
|
|
5804
|
+
a.setAttribute("data-icen-fv-name", name);
|
|
5805
|
+
a.textContent = `${fieldLabel(field, name)}\uFF1A${errorsObj[name]}`;
|
|
5806
|
+
li.appendChild(a);
|
|
5807
|
+
list.appendChild(li);
|
|
5808
|
+
}
|
|
5809
|
+
box.append(title, list);
|
|
5810
|
+
}
|
|
5811
|
+
function createFormValidator(form, opts = {}) {
|
|
5812
|
+
if (typeof document === "undefined") {
|
|
5813
|
+
const stub = () => void 0;
|
|
5814
|
+
return {
|
|
5815
|
+
form,
|
|
5816
|
+
validate: async () => true,
|
|
5817
|
+
validateField: async () => true,
|
|
5818
|
+
reset: stub,
|
|
5819
|
+
errors: () => ({}),
|
|
5820
|
+
destroy: stub
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5823
|
+
const existing = liveValidators.get(form);
|
|
5824
|
+
if (existing) return existing;
|
|
5825
|
+
const state = {
|
|
5826
|
+
errors: /* @__PURE__ */ new Map(),
|
|
5827
|
+
touched: /* @__PURE__ */ new Set(),
|
|
5828
|
+
submitted: false,
|
|
5829
|
+
seq: /* @__PURE__ */ new Map(),
|
|
5830
|
+
debounceTimers: /* @__PURE__ */ new Map()
|
|
5831
|
+
};
|
|
5832
|
+
let pointerDown = false;
|
|
5833
|
+
const pendingPresent = /* @__PURE__ */ new Map();
|
|
5834
|
+
let pendingSummary = null;
|
|
5835
|
+
let flushScheduled = false;
|
|
5836
|
+
const showValidFor = (field) => field.hasAttribute("data-icen-validate-show-valid") || form.hasAttribute("data-icen-validate-show-valid");
|
|
5837
|
+
async function checkField(field) {
|
|
5838
|
+
const name = field.name;
|
|
5839
|
+
const rules = resolveFieldRules(field);
|
|
5840
|
+
if (rules.length === 0) return true;
|
|
5841
|
+
trackComposition(field);
|
|
5842
|
+
const seq = (state.seq.get(name) ?? 0) + 1;
|
|
5843
|
+
state.seq.set(name, seq);
|
|
5844
|
+
const hasAsync = rules.some((r) => customValidators.has(r.rule));
|
|
5845
|
+
if (hasAsync) setValidating(field, true);
|
|
5846
|
+
const result = await evaluateRules(fieldValue(field), rules, { field, form });
|
|
5847
|
+
if (state.seq.get(name) !== seq) {
|
|
5848
|
+
if (hasAsync) setValidating(field, false);
|
|
5849
|
+
return !state.errors.has(name);
|
|
5850
|
+
}
|
|
5851
|
+
if (hasAsync) setValidating(field, false);
|
|
5852
|
+
const outcome = result === true ? { ok: true } : { ok: false, message: result };
|
|
5853
|
+
if (outcome.ok) state.errors.delete(name);
|
|
5854
|
+
else state.errors.set(name, outcome.message ?? "");
|
|
5855
|
+
presentFieldGuarded(field, outcome, showValidFor(field));
|
|
5856
|
+
if (form.querySelector(":scope > .icen-form-validate-summary")) {
|
|
5857
|
+
renderSummaryGuarded(errors());
|
|
5858
|
+
}
|
|
5859
|
+
emitIcen(
|
|
5860
|
+
field,
|
|
5861
|
+
"icen:form-field-validate",
|
|
5862
|
+
outcome.ok ? { name, valid: true } : { name, valid: false, message: outcome.message }
|
|
5863
|
+
);
|
|
5864
|
+
return outcome.ok;
|
|
5865
|
+
}
|
|
5866
|
+
function debouncedCheck(field) {
|
|
5867
|
+
const name = field.name;
|
|
5868
|
+
const timer = state.debounceTimers.get(name);
|
|
5869
|
+
if (timer) clearTimeout(timer);
|
|
5870
|
+
const delay = opts.strategy?.debounceMs ?? DEFAULT_DEBOUNCE;
|
|
5871
|
+
state.debounceTimers.set(
|
|
5872
|
+
name,
|
|
5873
|
+
setTimeout(() => {
|
|
5874
|
+
state.debounceTimers.delete(name);
|
|
5875
|
+
void checkField(field);
|
|
5876
|
+
}, delay)
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
function presentFieldPressed(field, outcome, showValid) {
|
|
5880
|
+
pendingPresent.set(field, { outcome, showValid });
|
|
5881
|
+
const wrap = fieldWrap(field);
|
|
5882
|
+
setValidating(field, false);
|
|
5883
|
+
if (!wrap) {
|
|
5884
|
+
if (!outcome.ok) field.setAttribute("aria-invalid", "true");
|
|
5885
|
+
else field.removeAttribute("aria-invalid");
|
|
5886
|
+
return;
|
|
5887
|
+
}
|
|
5888
|
+
if (!outcome.ok) {
|
|
5889
|
+
wrap.classList.add("is-error");
|
|
5890
|
+
wrap.setAttribute("data-icen-invalid", "true");
|
|
5891
|
+
wrap.classList.remove("is-valid");
|
|
5892
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5893
|
+
field.setAttribute("aria-invalid", "true");
|
|
5894
|
+
const errEl = findErrorEl(wrap);
|
|
5895
|
+
if (errEl && !errEl.hidden) errEl.textContent = outcome.message ?? "";
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
wrap.classList.remove("is-error");
|
|
5899
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5900
|
+
field.removeAttribute("aria-invalid");
|
|
5901
|
+
if (showValid) {
|
|
5902
|
+
wrap.classList.add("is-valid");
|
|
5903
|
+
wrap.setAttribute("data-icen-valid", "true");
|
|
5904
|
+
} else {
|
|
5905
|
+
wrap.classList.remove("is-valid");
|
|
5906
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
function presentFieldGuarded(field, outcome, showValid) {
|
|
5910
|
+
if (pointerDown) presentFieldPressed(field, outcome, showValid);
|
|
5911
|
+
else presentField(field, outcome, showValid);
|
|
5912
|
+
}
|
|
5913
|
+
function renderSummaryGuarded(errorsObj) {
|
|
5914
|
+
if (pointerDown) pendingSummary = errorsObj;
|
|
5915
|
+
else renderSummary(form, errorsObj);
|
|
5916
|
+
}
|
|
5917
|
+
function flushDeferredPresentation() {
|
|
5918
|
+
flushScheduled = false;
|
|
5919
|
+
if (destroyed) return;
|
|
5920
|
+
const items = Array.from(pendingPresent.entries());
|
|
5921
|
+
pendingPresent.clear();
|
|
5922
|
+
const summary = pendingSummary;
|
|
5923
|
+
pendingSummary = null;
|
|
5924
|
+
for (const [field, it] of items) presentField(field, it.outcome, it.showValid);
|
|
5925
|
+
if (summary) renderSummary(form, summary);
|
|
5926
|
+
}
|
|
5927
|
+
function scheduleFlush() {
|
|
5928
|
+
if (flushScheduled) return;
|
|
5929
|
+
flushScheduled = true;
|
|
5930
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flushDeferredPresentation);
|
|
5931
|
+
else setTimeout(flushDeferredPresentation, 0);
|
|
5932
|
+
}
|
|
5933
|
+
function resolveEventField(target) {
|
|
5934
|
+
if (!(target instanceof Element)) return null;
|
|
5935
|
+
const el = closestSelfOrAncestor(target, "input, textarea, select");
|
|
5936
|
+
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
|
|
5937
|
+
return null;
|
|
5938
|
+
}
|
|
5939
|
+
if (el.form !== form || el.disabled || !el.name) return null;
|
|
5940
|
+
if (el instanceof HTMLInputElement) {
|
|
5941
|
+
const type = (el.getAttribute("type") ?? "").toLowerCase();
|
|
5942
|
+
if (["submit", "button", "reset", "image", "file"].includes(type)) return null;
|
|
5943
|
+
}
|
|
5944
|
+
return el;
|
|
5945
|
+
}
|
|
5946
|
+
const onSubmit = (e) => {
|
|
5947
|
+
e.preventDefault();
|
|
5948
|
+
void validate();
|
|
5949
|
+
};
|
|
5950
|
+
const onFocusOut = (e) => {
|
|
5951
|
+
const field = resolveEventField(e.target);
|
|
5952
|
+
if (!field) return;
|
|
5953
|
+
state.touched.add(field.name);
|
|
5954
|
+
const mode = opts.strategy?.on;
|
|
5955
|
+
if (mode === "blur") void checkField(field);
|
|
5956
|
+
else if (!mode && state.submitted) void checkField(field);
|
|
5957
|
+
};
|
|
5958
|
+
const onInput = (e) => {
|
|
5959
|
+
const field = resolveEventField(e.target);
|
|
5960
|
+
if (!field) return;
|
|
5961
|
+
trackComposition(field);
|
|
5962
|
+
if (isComposing(field)) return;
|
|
5963
|
+
state.touched.add(field.name);
|
|
5964
|
+
const mode = opts.strategy?.on;
|
|
5965
|
+
if (mode === "input") debouncedCheck(field);
|
|
5966
|
+
else if (!mode && state.submitted && state.errors.has(field.name)) debouncedCheck(field);
|
|
5967
|
+
};
|
|
5968
|
+
async function validate() {
|
|
5969
|
+
state.submitted = true;
|
|
5970
|
+
const fields = collectFormFields(form);
|
|
5971
|
+
const names = [];
|
|
5972
|
+
let firstInvalid = null;
|
|
5973
|
+
for (const field of fields) {
|
|
5974
|
+
if (resolveFieldRules(field).length === 0) continue;
|
|
5975
|
+
names.push(field.name);
|
|
5976
|
+
const ok = await checkField(field);
|
|
5977
|
+
if (!ok && !firstInvalid) firstInvalid = field;
|
|
5978
|
+
}
|
|
5979
|
+
const errorsSnapshot = errors();
|
|
5980
|
+
if (firstInvalid) {
|
|
5981
|
+
renderSummaryGuarded(errorsSnapshot);
|
|
5982
|
+
emitIcen(form, "icen:form-invalid", { errors: errorsSnapshot, first: firstInvalid.name });
|
|
5983
|
+
if (opts.scrollToError !== false) focusField(firstInvalid);
|
|
5984
|
+
return false;
|
|
5985
|
+
}
|
|
5986
|
+
renderSummaryGuarded({});
|
|
5987
|
+
emitIcen(form, "icen:form-valid", { names });
|
|
5988
|
+
return true;
|
|
5989
|
+
}
|
|
5990
|
+
function errors() {
|
|
5991
|
+
return Object.fromEntries(state.errors);
|
|
5992
|
+
}
|
|
5993
|
+
function reset() {
|
|
5994
|
+
state.errors.clear();
|
|
5995
|
+
state.touched.clear();
|
|
5996
|
+
state.submitted = false;
|
|
5997
|
+
state.seq.clear();
|
|
5998
|
+
for (const timer of state.debounceTimers.values()) clearTimeout(timer);
|
|
5999
|
+
state.debounceTimers.clear();
|
|
6000
|
+
pendingPresent.clear();
|
|
6001
|
+
pendingSummary = null;
|
|
6002
|
+
for (const field of collectFormFields(form)) clearFieldPresentation(field);
|
|
6003
|
+
form.querySelector(":scope > .icen-form-validate-summary")?.remove();
|
|
6004
|
+
}
|
|
6005
|
+
const onFormPointerDown = (e) => {
|
|
6006
|
+
const btn = e.button;
|
|
6007
|
+
if (typeof btn === "number" && btn !== 0) return;
|
|
6008
|
+
pointerDown = true;
|
|
6009
|
+
};
|
|
6010
|
+
const onPressEnd = () => {
|
|
6011
|
+
if (!pointerDown) return;
|
|
6012
|
+
pointerDown = false;
|
|
6013
|
+
if (pendingPresent.size > 0 || pendingSummary !== null) scheduleFlush();
|
|
6014
|
+
};
|
|
6015
|
+
form.addEventListener("submit", onSubmit);
|
|
6016
|
+
form.addEventListener("focusout", onFocusOut);
|
|
6017
|
+
form.addEventListener("input", onInput);
|
|
6018
|
+
form.addEventListener("pointerdown", onFormPointerDown, true);
|
|
6019
|
+
document.addEventListener("pointerup", onPressEnd, true);
|
|
6020
|
+
document.addEventListener("pointercancel", onPressEnd, true);
|
|
6021
|
+
window.addEventListener("blur", onPressEnd);
|
|
6022
|
+
let destroyed = false;
|
|
6023
|
+
const handle = {
|
|
6024
|
+
form,
|
|
6025
|
+
validate,
|
|
6026
|
+
async validateField(name) {
|
|
6027
|
+
const field = findFormField(form, name);
|
|
6028
|
+
if (!field || field.disabled) return true;
|
|
6029
|
+
return checkField(field);
|
|
6030
|
+
},
|
|
6031
|
+
reset,
|
|
6032
|
+
errors,
|
|
6033
|
+
destroy() {
|
|
6034
|
+
if (destroyed) return;
|
|
6035
|
+
destroyed = true;
|
|
6036
|
+
form.removeEventListener("submit", onSubmit);
|
|
6037
|
+
form.removeEventListener("focusout", onFocusOut);
|
|
6038
|
+
form.removeEventListener("input", onInput);
|
|
6039
|
+
form.removeEventListener("pointerdown", onFormPointerDown, true);
|
|
6040
|
+
document.removeEventListener("pointerup", onPressEnd, true);
|
|
6041
|
+
document.removeEventListener("pointercancel", onPressEnd, true);
|
|
6042
|
+
window.removeEventListener("blur", onPressEnd);
|
|
6043
|
+
reset();
|
|
6044
|
+
liveValidators.delete(form);
|
|
6045
|
+
}
|
|
6046
|
+
};
|
|
6047
|
+
liveValidators.set(form, handle);
|
|
6048
|
+
return handle;
|
|
6049
|
+
}
|
|
6050
|
+
var SHOW_FORM_SCHEMA = {
|
|
6051
|
+
type: "object",
|
|
6052
|
+
properties: {
|
|
6053
|
+
title: { type: "string", description: t("formValidate.tool.title") },
|
|
6054
|
+
fields: {
|
|
6055
|
+
type: "array",
|
|
6056
|
+
description: t("formValidate.tool.fields"),
|
|
6057
|
+
items: {
|
|
6058
|
+
type: "object",
|
|
6059
|
+
properties: {
|
|
6060
|
+
name: { type: "string", description: t("formValidate.tool.name") },
|
|
6061
|
+
label: { type: "string", description: t("formValidate.tool.label") },
|
|
6062
|
+
type: {
|
|
6063
|
+
type: "string",
|
|
6064
|
+
enum: ["text", "email", "number", "url", "textarea", "select", "date"],
|
|
6065
|
+
description: t("formValidate.tool.type")
|
|
6066
|
+
},
|
|
6067
|
+
required: { type: "boolean", description: t("formValidate.tool.required") },
|
|
6068
|
+
pattern: { type: "string", description: t("formValidate.tool.pattern") },
|
|
6069
|
+
min: { type: "number", description: t("formValidate.tool.min") },
|
|
6070
|
+
max: { type: "number", description: t("formValidate.tool.max") },
|
|
6071
|
+
options: {
|
|
6072
|
+
type: "array",
|
|
6073
|
+
description: t("formValidate.tool.options"),
|
|
6074
|
+
items: {
|
|
6075
|
+
anyOf: [
|
|
6076
|
+
{ type: "string" },
|
|
6077
|
+
{
|
|
6078
|
+
type: "object",
|
|
6079
|
+
properties: { label: { type: "string" }, value: { type: "string" } },
|
|
6080
|
+
required: ["label", "value"]
|
|
6081
|
+
}
|
|
6082
|
+
]
|
|
6083
|
+
}
|
|
6084
|
+
},
|
|
6085
|
+
help: { type: "string", description: t("formValidate.tool.help") }
|
|
6086
|
+
},
|
|
6087
|
+
required: ["name", "label"]
|
|
6088
|
+
}
|
|
6089
|
+
},
|
|
6090
|
+
submitLabel: { type: "string", description: t("formValidate.tool.submitText") }
|
|
6091
|
+
},
|
|
6092
|
+
required: ["fields"]
|
|
6093
|
+
};
|
|
6094
|
+
function normalizeShowForm(input) {
|
|
6095
|
+
const raw = input ?? {};
|
|
6096
|
+
const fields = [];
|
|
6097
|
+
if (Array.isArray(raw.fields)) {
|
|
6098
|
+
for (const f of raw.fields) {
|
|
6099
|
+
if (!f || typeof f !== "object") continue;
|
|
6100
|
+
const name = typeof f.name === "string" ? f.name.trim() : "";
|
|
6101
|
+
if (!name) continue;
|
|
6102
|
+
const label = typeof f.label === "string" && f.label ? f.label : name;
|
|
6103
|
+
fields.push({
|
|
6104
|
+
name,
|
|
6105
|
+
label,
|
|
6106
|
+
type: f.type,
|
|
6107
|
+
required: f.required === true,
|
|
6108
|
+
pattern: typeof f.pattern === "string" ? f.pattern : void 0,
|
|
6109
|
+
min: typeof f.min === "number" ? f.min : void 0,
|
|
6110
|
+
max: typeof f.max === "number" ? f.max : void 0,
|
|
6111
|
+
options: Array.isArray(f.options) ? f.options : void 0,
|
|
6112
|
+
help: typeof f.help === "string" ? f.help : void 0
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
return {
|
|
6117
|
+
title: typeof raw.title === "string" && raw.title ? raw.title : void 0,
|
|
6118
|
+
fields,
|
|
6119
|
+
submitLabel: typeof raw.submitLabel === "string" && raw.submitLabel ? raw.submitLabel : void 0
|
|
6120
|
+
};
|
|
6121
|
+
}
|
|
6122
|
+
function renderShowFormField(spec) {
|
|
6123
|
+
const wrap = document.createElement("div");
|
|
6124
|
+
wrap.className = "icen-field";
|
|
6125
|
+
const id = `icen-ai-form-${++uidSeq}`;
|
|
6126
|
+
const label = document.createElement("label");
|
|
6127
|
+
label.className = "icen-field-label";
|
|
6128
|
+
label.htmlFor = id;
|
|
6129
|
+
label.textContent = spec.label;
|
|
6130
|
+
if (spec.required) {
|
|
6131
|
+
const req = document.createElement("span");
|
|
6132
|
+
req.className = "icen-req";
|
|
6133
|
+
req.textContent = "*";
|
|
6134
|
+
label.appendChild(req);
|
|
6135
|
+
}
|
|
6136
|
+
wrap.appendChild(label);
|
|
6137
|
+
let control;
|
|
6138
|
+
if (spec.type === "textarea") {
|
|
6139
|
+
const ta = document.createElement("textarea");
|
|
6140
|
+
ta.className = "icen-form-textarea";
|
|
6141
|
+
ta.setAttribute("rows", "3");
|
|
6142
|
+
control = ta;
|
|
6143
|
+
} else if (spec.type === "select") {
|
|
6144
|
+
const sel = document.createElement("select");
|
|
6145
|
+
sel.className = "icen-form-input";
|
|
6146
|
+
const placeholder = document.createElement("option");
|
|
6147
|
+
placeholder.value = "";
|
|
6148
|
+
placeholder.textContent = t("formValidate.selectPlaceholder");
|
|
6149
|
+
sel.appendChild(placeholder);
|
|
6150
|
+
for (const opt of spec.options ?? []) {
|
|
6151
|
+
const option = document.createElement("option");
|
|
6152
|
+
if (typeof opt === "string") {
|
|
6153
|
+
option.value = opt;
|
|
6154
|
+
option.textContent = opt;
|
|
6155
|
+
} else {
|
|
6156
|
+
option.value = opt.value;
|
|
6157
|
+
option.textContent = opt.label;
|
|
6158
|
+
}
|
|
6159
|
+
sel.appendChild(option);
|
|
6160
|
+
}
|
|
6161
|
+
control = sel;
|
|
6162
|
+
} else {
|
|
6163
|
+
const input = document.createElement("input");
|
|
6164
|
+
input.className = "icen-form-input";
|
|
6165
|
+
input.type = spec.type ?? "text";
|
|
6166
|
+
control = input;
|
|
6167
|
+
}
|
|
6168
|
+
control.id = id;
|
|
6169
|
+
control.name = spec.name;
|
|
6170
|
+
if (spec.required) control.required = true;
|
|
6171
|
+
if (spec.pattern) control.setAttribute("pattern", spec.pattern);
|
|
6172
|
+
if (spec.min != null) control.setAttribute("min", String(spec.min));
|
|
6173
|
+
if (spec.max != null) control.setAttribute("max", String(spec.max));
|
|
6174
|
+
wrap.appendChild(control);
|
|
6175
|
+
if (spec.help) {
|
|
6176
|
+
const hint = document.createElement("span");
|
|
6177
|
+
hint.className = "icen-field-hint";
|
|
6178
|
+
hint.textContent = spec.help;
|
|
6179
|
+
wrap.appendChild(hint);
|
|
6180
|
+
}
|
|
6181
|
+
return wrap;
|
|
6182
|
+
}
|
|
6183
|
+
function collectFormValues(form) {
|
|
6184
|
+
const out = {};
|
|
6185
|
+
for (const field of collectFormFields(form)) {
|
|
6186
|
+
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
6187
|
+
out[field.name] = field.checked;
|
|
6188
|
+
continue;
|
|
6189
|
+
}
|
|
6190
|
+
if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
6191
|
+
if (!(field.name in out)) out[field.name] = field.checked ? field.value : "";
|
|
6192
|
+
else if (field.checked) out[field.name] = field.value;
|
|
6193
|
+
continue;
|
|
6194
|
+
}
|
|
6195
|
+
out[field.name] = field.value;
|
|
6196
|
+
}
|
|
6197
|
+
return out;
|
|
6198
|
+
}
|
|
6199
|
+
function runShowForm(input, mount) {
|
|
6200
|
+
return new Promise((resolve) => {
|
|
6201
|
+
if (typeof document === "undefined") {
|
|
6202
|
+
resolve({ cancelled: true });
|
|
6203
|
+
return;
|
|
6204
|
+
}
|
|
6205
|
+
const spec = normalizeShowForm(input);
|
|
6206
|
+
const root = document.createElement("div");
|
|
6207
|
+
root.className = "icen-ai-form";
|
|
6208
|
+
if (spec.title) {
|
|
6209
|
+
const title = document.createElement("p");
|
|
6210
|
+
title.className = "icen-ai-form-title";
|
|
6211
|
+
title.textContent = spec.title;
|
|
6212
|
+
root.appendChild(title);
|
|
6213
|
+
}
|
|
6214
|
+
const form = document.createElement("form");
|
|
6215
|
+
form.className = "icen-form icen-ai-form-fields";
|
|
6216
|
+
form.setAttribute("data-icen-validate", "");
|
|
6217
|
+
form.setAttribute("data-icen-validate-summary", "");
|
|
6218
|
+
form.setAttribute("novalidate", "");
|
|
6219
|
+
for (const field of spec.fields) form.appendChild(renderShowFormField(field));
|
|
6220
|
+
const actions = document.createElement("div");
|
|
6221
|
+
actions.className = "icen-ai-form-actions";
|
|
6222
|
+
const submit = document.createElement("button");
|
|
6223
|
+
submit.type = "submit";
|
|
6224
|
+
submit.className = "icen-btn icen-btn-primary";
|
|
6225
|
+
submit.textContent = spec.submitLabel ?? t("common.submit");
|
|
6226
|
+
const cancel = document.createElement("button");
|
|
6227
|
+
cancel.type = "button";
|
|
6228
|
+
cancel.className = "icen-btn icen-btn-ghost";
|
|
6229
|
+
cancel.setAttribute("data-icen-ai-form-cancel", "");
|
|
6230
|
+
cancel.textContent = t("common.cancel");
|
|
6231
|
+
actions.append(submit, cancel);
|
|
6232
|
+
form.appendChild(actions);
|
|
6233
|
+
root.appendChild(form);
|
|
6234
|
+
mount.appendChild(root);
|
|
6235
|
+
const handle = createFormValidator(form, { scrollToError: true });
|
|
6236
|
+
const finish = (value) => {
|
|
6237
|
+
form.removeEventListener("icen:form-valid", onValid);
|
|
6238
|
+
cancel.removeEventListener("click", onCancel);
|
|
6239
|
+
handle.destroy();
|
|
6240
|
+
root.remove();
|
|
6241
|
+
resolve(value);
|
|
6242
|
+
};
|
|
6243
|
+
const onValid = () => {
|
|
6244
|
+
finish(collectFormValues(form));
|
|
6245
|
+
};
|
|
6246
|
+
const onCancel = () => {
|
|
6247
|
+
finish({ cancelled: true });
|
|
6248
|
+
};
|
|
6249
|
+
form.addEventListener("icen:form-valid", onValid);
|
|
6250
|
+
cancel.addEventListener("click", onCancel);
|
|
6251
|
+
});
|
|
6252
|
+
}
|
|
6253
|
+
|
|
5110
6254
|
// src/behaviors/ai-tools.ts
|
|
5111
6255
|
var toolRegistry = /* @__PURE__ */ new Map();
|
|
5112
6256
|
var LIVE_AREAS = /* @__PURE__ */ new Set();
|
|
@@ -5435,6 +6579,13 @@ registerAiTool({
|
|
|
5435
6579
|
return { title, rows: rows.length };
|
|
5436
6580
|
}
|
|
5437
6581
|
});
|
|
6582
|
+
registerAiTool({
|
|
6583
|
+
name: "show_form",
|
|
6584
|
+
description: t("formValidate.tool.description"),
|
|
6585
|
+
inputSchema: SHOW_FORM_SCHEMA,
|
|
6586
|
+
present: "mount",
|
|
6587
|
+
run: (input, mount) => runShowForm(input, mount)
|
|
6588
|
+
});
|
|
5438
6589
|
function isBrowserTools() {
|
|
5439
6590
|
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
5440
6591
|
}
|