@icen.ai/ui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +79 -309
- package/LICENSE +21 -0
- package/README.md +43 -54
- package/dist/base.css +57 -16
- package/dist/behaviors/accordion.mjs +1 -1
- package/dist/behaviors/ai-branch.mjs +1 -0
- package/dist/behaviors/ai-chat.d.mts +2 -2
- package/dist/behaviors/ai-chat.mjs +53 -10
- package/dist/behaviors/ai-composer.d.mts +5 -5
- package/dist/behaviors/ai-composer.mjs +21 -10
- package/dist/behaviors/ai-core.mjs +1 -0
- package/dist/behaviors/ai-diff.mjs +1 -0
- package/dist/behaviors/ai-feedback.mjs +1 -0
- package/dist/behaviors/ai-panel.d.mts +2 -2
- package/dist/behaviors/ai-panel.mjs +1 -0
- package/dist/behaviors/ai-provider.d.mts +1 -1
- package/dist/behaviors/ai-provider.mjs +1 -0
- package/dist/behaviors/ai-threads.mjs +1 -0
- package/dist/behaviors/ai-tool.d.mts +1 -1
- package/dist/behaviors/ai-tool.mjs +57 -24
- package/dist/behaviors/ai-tools.mjs +937 -28
- package/dist/behaviors/autocomplete.d.mts +1 -1
- package/dist/behaviors/autocomplete.mjs +1 -0
- package/dist/behaviors/back-top.mjs +1 -0
- package/dist/behaviors/carousel.mjs +8 -1
- package/dist/behaviors/cascader.mjs +14 -0
- package/dist/behaviors/charts.mjs +56 -20
- package/dist/behaviors/code-block.mjs +9 -1
- package/dist/behaviors/command-palette.mjs +3 -3
- package/dist/behaviors/context-menu.d.mts +2 -2
- package/dist/behaviors/context-menu.mjs +1 -0
- package/dist/behaviors/controls.d.mts +8 -8
- package/dist/behaviors/controls.mjs +37 -6
- package/dist/behaviors/copy.mjs +1 -0
- package/dist/behaviors/datatable.d.mts +3 -3
- package/dist/behaviors/datatable.mjs +76 -11
- package/dist/behaviors/date-picker.mjs +1 -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 +266 -4611
- package/dist/behaviors/i18n.d.mts +1 -0
- package/dist/behaviors/i18n.mjs +2 -0
- package/dist/behaviors/input.mjs +3 -0
- package/dist/behaviors/kb-access.d.mts +2 -2
- package/dist/behaviors/kb-access.mjs +8 -2
- package/dist/behaviors/kb-acl.d.mts +2 -2
- package/dist/behaviors/kb-acl.mjs +1 -0
- package/dist/behaviors/kb-answer.d.mts +2 -2
- package/dist/behaviors/kb-answer.mjs +56 -23
- package/dist/behaviors/kb-audit.d.mts +2 -2
- package/dist/behaviors/kb-audit.mjs +1 -0
- package/dist/behaviors/kb-canvas.d.mts +3 -3
- package/dist/behaviors/kb-canvas.mjs +1 -0
- package/dist/behaviors/kb-chain.d.mts +2 -2
- package/dist/behaviors/kb-chain.mjs +1 -0
- package/dist/behaviors/kb-checkpoint.d.mts +2 -2
- package/dist/behaviors/kb-checkpoint.mjs +1 -0
- package/dist/behaviors/kb-chunks.d.mts +2 -2
- package/dist/behaviors/kb-chunks.mjs +1 -0
- package/dist/behaviors/kb-citation.d.mts +2 -2
- package/dist/behaviors/kb-citation.mjs +20 -6
- package/dist/behaviors/kb-clarify.d.mts +2 -2
- package/dist/behaviors/kb-clarify.mjs +1 -0
- package/dist/behaviors/kb-conflict.d.mts +2 -2
- package/dist/behaviors/kb-conflict.mjs +1 -0
- package/dist/behaviors/kb-connector.d.mts +2 -2
- package/dist/behaviors/kb-connector.mjs +1 -0
- package/dist/behaviors/kb-core.d.mts +1 -1
- package/dist/behaviors/kb-core.mjs +1 -0
- package/dist/behaviors/kb-eval.d.mts +2 -2
- package/dist/behaviors/kb-eval.mjs +8 -2
- package/dist/behaviors/kb-explain.d.mts +2 -2
- package/dist/behaviors/kb-explain.mjs +13 -2
- package/dist/behaviors/kb-filter.d.mts +2 -2
- package/dist/behaviors/kb-filter.mjs +8 -2
- package/dist/behaviors/kb-gap.d.mts +2 -2
- package/dist/behaviors/kb-gap.mjs +1 -0
- package/dist/behaviors/kb-hittest.d.mts +2 -2
- package/dist/behaviors/kb-hittest.mjs +2 -1
- package/dist/behaviors/kb-metadata.mjs +6 -0
- package/dist/behaviors/kb-passage.d.mts +2 -2
- package/dist/behaviors/kb-passage.mjs +1 -0
- package/dist/behaviors/kb-pipeline.d.mts +2 -2
- package/dist/behaviors/kb-pipeline.mjs +1 -0
- package/dist/behaviors/kb-qa.mjs +1 -0
- package/dist/behaviors/kb-rerank.d.mts +2 -2
- package/dist/behaviors/kb-rerank.mjs +1 -0
- package/dist/behaviors/kb-retrieval.d.mts +2 -2
- package/dist/behaviors/kb-retrieval.mjs +18 -4
- package/dist/behaviors/kb-review.d.mts +2 -2
- package/dist/behaviors/kb-review.mjs +8 -2
- package/dist/behaviors/kb-sandbox.d.mts +2 -2
- package/dist/behaviors/kb-sandbox.mjs +7 -1
- package/dist/behaviors/kb-segment.d.mts +2 -2
- package/dist/behaviors/kb-segment.mjs +2 -1
- package/dist/behaviors/kb-select.mjs +8 -2
- package/dist/behaviors/kb-sources.d.mts +2 -2
- package/dist/behaviors/kb-sources.mjs +1 -0
- package/dist/behaviors/kb-sql.d.mts +2 -2
- package/dist/behaviors/kb-sql.mjs +1 -0
- package/dist/behaviors/kb-trace.d.mts +2 -2
- package/dist/behaviors/kb-trace.mjs +9 -3
- package/dist/behaviors/kb-visibility.d.mts +2 -2
- package/dist/behaviors/kb-visibility.mjs +1 -0
- package/dist/behaviors/kb-who-can.d.mts +2 -2
- package/dist/behaviors/kb-who-can.mjs +8 -2
- package/dist/behaviors/mentions.d.mts +1 -1
- package/dist/behaviors/mentions.mjs +1 -0
- package/dist/behaviors/modal.d.mts +1 -1
- package/dist/behaviors/modal.mjs +1 -0
- package/dist/behaviors/notification.d.mts +2 -2
- package/dist/behaviors/notification.mjs +14 -3
- package/dist/behaviors/panel.mjs +1 -0
- package/dist/behaviors/scroll-area.d.mts +17 -0
- package/dist/behaviors/scroll-area.mjs +59 -0
- package/dist/behaviors/select.mjs +8 -2
- package/dist/behaviors/slider.mjs +3 -3
- package/dist/behaviors/split-pane.mjs +2 -0
- package/dist/behaviors/suggestions.mjs +1 -0
- package/dist/behaviors/tabs.mjs +10 -0
- package/dist/behaviors/tag-input.mjs +1 -0
- package/dist/behaviors/theme-switcher.d.mts +2 -2
- package/dist/behaviors/theme-switcher.mjs +17 -2
- package/dist/behaviors/theme.mjs +16 -2
- package/dist/behaviors/toast.d.mts +2 -2
- package/dist/behaviors/toast.mjs +1 -0
- package/dist/behaviors/tree-select.mjs +14 -0
- package/dist/behaviors/tree.mjs +4 -4
- package/dist/behaviors/upload.mjs +1 -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 +80 -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 +14764 -14311
- package/dist/react.d.mts +18 -18
- package/dist/react.mjs +135 -36
- package/dist/registry.json +795 -64
- package/dist/tokens/colors.css +6 -0
- package/dist/tokens/design.css +130 -30
- package/dist/tokens/dtcg.json +6189 -1461
- package/dist/tokens/figma.variables.json +4107 -345
- package/dist/tokens/pixel.css +3 -0
- package/dist/tokens/scrollbars.css +40 -0
- package/dist/tokens/tokens-studio.json +5191 -271
- package/dist/tokens.css +304 -81
- package/dist/ui.css +4050 -2148
- package/dist/vue.d.mts +17 -17
- package/dist/vue.mjs +129 -34
- package/docs/spec/design-philosophy.md +5 -5
- package/docs/spec/design-primitives.md +10 -10
- package/docs/spec/scrollbars.md +46 -0
- package/package.json +16 -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",
|
|
@@ -1163,7 +1164,8 @@ function withChrome(el, opts, hasLegend, paint, state, extras) {
|
|
|
1163
1164
|
emitIcen(el, "icen:chart-legend-visibility", { visible: hidden ? false : true });
|
|
1164
1165
|
});
|
|
1165
1166
|
syncEye();
|
|
1166
|
-
head.append(title, eye);
|
|
1167
|
+
if (hasLegend) head.append(title, eye);
|
|
1168
|
+
else head.append(title);
|
|
1167
1169
|
el.append(head, body);
|
|
1168
1170
|
paint(body);
|
|
1169
1171
|
}
|
|
@@ -1264,7 +1266,7 @@ function renderVBar(el, opts) {
|
|
|
1264
1266
|
wrap.appendChild(track);
|
|
1265
1267
|
}
|
|
1266
1268
|
body.append(wrap, labelsRow(labelsUsed));
|
|
1267
|
-
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name
|
|
1269
|
+
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
1270
|
});
|
|
1269
1271
|
attachChartInteraction(el, "vbar");
|
|
1270
1272
|
return el;
|
|
@@ -1359,8 +1361,10 @@ function renderDonut(el, opts) {
|
|
|
1359
1361
|
"stroke-width": "14"
|
|
1360
1362
|
}));
|
|
1361
1363
|
let offset = 0;
|
|
1364
|
+
const gap = opts.segments.length > 1 ? 2.5 : 0;
|
|
1362
1365
|
opts.segments.forEach((seg, segIndex) => {
|
|
1363
|
-
const
|
|
1366
|
+
const arcLength = Math.max(0, seg.value / total * circumference);
|
|
1367
|
+
const dash = arcLength - Math.min(gap, arcLength * 0.1);
|
|
1364
1368
|
const arc = svgEl("circle", {
|
|
1365
1369
|
cx: "60",
|
|
1366
1370
|
cy: "60",
|
|
@@ -1374,7 +1378,7 @@ function renderDonut(el, opts) {
|
|
|
1374
1378
|
});
|
|
1375
1379
|
setMarkData(arc, { index: segIndex, label: seg.label, value: seg.value });
|
|
1376
1380
|
svg.appendChild(arc);
|
|
1377
|
-
offset +=
|
|
1381
|
+
offset += arcLength;
|
|
1378
1382
|
});
|
|
1379
1383
|
const legend = document.createElement("div");
|
|
1380
1384
|
legend.className = "icen-chart-legend";
|
|
@@ -1409,8 +1413,9 @@ function renderLine(el, opts) {
|
|
|
1409
1413
|
const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chartAriaLabel(opts.title, t("charts.type.line")) });
|
|
1410
1414
|
const defs = svgEl("defs", {});
|
|
1411
1415
|
for (let i = 0; i < 4; i++) {
|
|
1412
|
-
const y = pad + i * (height - pad * 2) / 3;
|
|
1416
|
+
const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
|
|
1413
1417
|
svg.appendChild(svgEl("line", {
|
|
1418
|
+
class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
|
|
1414
1419
|
x1: String(pad),
|
|
1415
1420
|
x2: String(width - pad),
|
|
1416
1421
|
y1: String(y),
|
|
@@ -1494,7 +1499,7 @@ function renderLine(el, opts) {
|
|
|
1494
1499
|
if (defs.firstChild) svg.insertBefore(defs, svg.firstChild);
|
|
1495
1500
|
wrap.append(svg, pointLabelsRow(labelsUsed, width, pad));
|
|
1496
1501
|
body.appendChild(wrap);
|
|
1497
|
-
if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name
|
|
1502
|
+
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
1503
|
});
|
|
1499
1504
|
if (el.dataset.icenChartBody == null) observeFluidWidth(el, () => renderLine(el, opts));
|
|
1500
1505
|
attachChartInteraction(el, "line");
|
|
@@ -1601,7 +1606,7 @@ function renderRadar(el, opts) {
|
|
|
1601
1606
|
setMarkData(dot, { index: i, seriesIndex: si, seriesName: s.name || void 0, label: axes[i] ?? "", value: s.values[i] ?? 0 });
|
|
1602
1607
|
svg.appendChild(dot);
|
|
1603
1608
|
}
|
|
1604
|
-
legendItems.push({ key: s.name || t("charts.seriesName", { p0: si + 1 })
|
|
1609
|
+
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
1610
|
});
|
|
1606
1611
|
wrap.appendChild(svg);
|
|
1607
1612
|
body.appendChild(wrap);
|
|
@@ -1623,6 +1628,11 @@ function niceCeil(v) {
|
|
|
1623
1628
|
else nice = 10;
|
|
1624
1629
|
return nice * base;
|
|
1625
1630
|
}
|
|
1631
|
+
function tickPrecision(min, max, steps) {
|
|
1632
|
+
const span = Math.abs(max - min);
|
|
1633
|
+
if (!Number.isFinite(span) || span <= 0) return 0;
|
|
1634
|
+
return Math.max(0, Math.ceil(-Math.log10(span / steps)));
|
|
1635
|
+
}
|
|
1626
1636
|
function isoDate(d) {
|
|
1627
1637
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1628
1638
|
const day = String(d.getDate()).padStart(2, "0");
|
|
@@ -1718,7 +1728,8 @@ function renderSparkline(el, opts) {
|
|
|
1718
1728
|
const tone = toneVar(opts.tone);
|
|
1719
1729
|
const points = values.map((value, i) => ({
|
|
1720
1730
|
x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
|
|
1721
|
-
|
|
1731
|
+
/* 负值钳到基线(与折线图同口径):viewBox 仅 28 高,反向射出更刺眼 */
|
|
1732
|
+
y: height - pad - Math.max(0, value) / max * (height - pad * 2),
|
|
1722
1733
|
value
|
|
1723
1734
|
}));
|
|
1724
1735
|
const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
|
|
@@ -1843,6 +1854,7 @@ function renderCalendar(el, opts) {
|
|
|
1843
1854
|
weekdayRow.appendChild(s);
|
|
1844
1855
|
});
|
|
1845
1856
|
const max = maxOf(entries.map((e) => e.value));
|
|
1857
|
+
entries.sort((a, b) => a.date.localeCompare(b.date));
|
|
1846
1858
|
const pad = padFirstWeek(grid, entries[0].date, weekStart);
|
|
1847
1859
|
let lastMonth = -1;
|
|
1848
1860
|
entries.forEach((entry, i) => {
|
|
@@ -1901,8 +1913,9 @@ function renderScatter(el, opts) {
|
|
|
1901
1913
|
const xAt = (x) => pad + (x - xMin) / (xMax - xMin) * (width - pad * 2);
|
|
1902
1914
|
const yAt = (y) => height - pad - (y - yMin) / (yMax - yMin) * (height - pad * 2);
|
|
1903
1915
|
for (let i = 0; i <= 3; i++) {
|
|
1904
|
-
const y = pad + i * (height - pad * 2) / 3;
|
|
1916
|
+
const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
|
|
1905
1917
|
svg.appendChild(svgEl("line", {
|
|
1918
|
+
class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
|
|
1906
1919
|
x1: String(pad),
|
|
1907
1920
|
x2: String(width - pad),
|
|
1908
1921
|
y1: String(y),
|
|
@@ -1918,12 +1931,13 @@ function renderScatter(el, opts) {
|
|
|
1918
1931
|
"font-size": "8",
|
|
1919
1932
|
fill: "var(--icen-token-text-faint)"
|
|
1920
1933
|
});
|
|
1921
|
-
txt.textContent = fmt(opts,
|
|
1934
|
+
txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(yMin, yMax, 3)));
|
|
1922
1935
|
svg.appendChild(txt);
|
|
1923
1936
|
}
|
|
1924
1937
|
for (let i = 0; i <= 4; i++) {
|
|
1925
|
-
const x = pad + i * (width - pad * 2) / 4;
|
|
1938
|
+
const x = Math.round(pad + i * (width - pad * 2) / 4) + 0.5;
|
|
1926
1939
|
svg.appendChild(svgEl("line", {
|
|
1940
|
+
class: i === 0 ? "icen-chart-grid icen-chart-grid--v icen-chart-axis" : "icen-chart-grid icen-chart-grid--v",
|
|
1927
1941
|
x1: String(x),
|
|
1928
1942
|
x2: String(x),
|
|
1929
1943
|
y1: String(pad),
|
|
@@ -1940,7 +1954,7 @@ function renderScatter(el, opts) {
|
|
|
1940
1954
|
"font-size": "8",
|
|
1941
1955
|
fill: "var(--icen-token-text-faint)"
|
|
1942
1956
|
});
|
|
1943
|
-
txt.textContent = fmt(opts,
|
|
1957
|
+
txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(xMin, xMax, 4)));
|
|
1944
1958
|
svg.appendChild(txt);
|
|
1945
1959
|
}
|
|
1946
1960
|
if (opts.yLabel) {
|
|
@@ -2304,6 +2318,7 @@ function renderChartSkeleton(body) {
|
|
|
2304
2318
|
body.appendChild(box);
|
|
2305
2319
|
}
|
|
2306
2320
|
var CHART_MARK_ACCENT = "var(--icen-token-accent)";
|
|
2321
|
+
var CHART_MARK_ACCENT_TEXT = "var(--icen-token-accent-text)";
|
|
2307
2322
|
function markToneVar(tone) {
|
|
2308
2323
|
switch (tone) {
|
|
2309
2324
|
case "success":
|
|
@@ -2439,7 +2454,7 @@ function paintSvgChartMarks(root, view, marks, formatValue, warned) {
|
|
|
2439
2454
|
y: String(clampNum(y - 10, 8, CHART_LINE_H - 4)),
|
|
2440
2455
|
"text-anchor": "middle",
|
|
2441
2456
|
"font-size": "8",
|
|
2442
|
-
fill:
|
|
2457
|
+
fill: CHART_MARK_ACCENT_TEXT
|
|
2443
2458
|
});
|
|
2444
2459
|
label.textContent = mark.text ?? mark.label;
|
|
2445
2460
|
g.appendChild(label);
|
|
@@ -2997,6 +3012,12 @@ function renderChart(el, raw) {
|
|
|
2997
3012
|
const graphic = root.querySelector('[role="img"]');
|
|
2998
3013
|
if (graphic && !graphic.getAttribute("aria-label")) {
|
|
2999
3014
|
graphic.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
|
|
3015
|
+
} else if (!graphic) {
|
|
3016
|
+
const dom = root.querySelector(".icen-chart-vbar, .icen-chart-hbar");
|
|
3017
|
+
if (dom) {
|
|
3018
|
+
dom.setAttribute("role", "img");
|
|
3019
|
+
dom.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
|
|
3020
|
+
}
|
|
3000
3021
|
}
|
|
3001
3022
|
}
|
|
3002
3023
|
if (loading) return;
|
|
@@ -3498,7 +3519,13 @@ function onKeyDown(e) {
|
|
|
3498
3519
|
|
|
3499
3520
|
// src/behaviors/datatable.ts
|
|
3500
3521
|
function defaultCompare(a, b) {
|
|
3501
|
-
if (typeof a === "number" && typeof b === "number")
|
|
3522
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
3523
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
3524
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return 0;
|
|
3525
|
+
return Number.isNaN(a) ? 1 : -1;
|
|
3526
|
+
}
|
|
3527
|
+
return a - b;
|
|
3528
|
+
}
|
|
3502
3529
|
return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
|
|
3503
3530
|
}
|
|
3504
3531
|
function toNum2(v) {
|
|
@@ -3787,11 +3814,13 @@ function createTable(el, opts) {
|
|
|
3787
3814
|
const body = document.createElement("div");
|
|
3788
3815
|
body.className = "icen-dt-body";
|
|
3789
3816
|
body.setAttribute("role", "rowgroup");
|
|
3790
|
-
const summaryEl = document.createElement("div");
|
|
3791
|
-
summaryEl
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3817
|
+
const summaryEl = opts.summaryRow ? document.createElement("div") : null;
|
|
3818
|
+
if (summaryEl) {
|
|
3819
|
+
summaryEl.className = "icen-dt-row icen-dt-row--summary";
|
|
3820
|
+
summaryEl.setAttribute("role", "row");
|
|
3821
|
+
summaryEl.hidden = true;
|
|
3822
|
+
}
|
|
3823
|
+
canvas.append(head, body, ...summaryEl ? [summaryEl] : []);
|
|
3795
3824
|
scroll.appendChild(canvas);
|
|
3796
3825
|
root.appendChild(scroll);
|
|
3797
3826
|
const foot = document.createElement("div");
|
|
@@ -4604,7 +4633,8 @@ function createTable(el, opts) {
|
|
|
4604
4633
|
else wrap.textContent = String(out);
|
|
4605
4634
|
return wrap;
|
|
4606
4635
|
}
|
|
4607
|
-
function
|
|
4636
|
+
function renderSummary2() {
|
|
4637
|
+
if (!summaryEl) return;
|
|
4608
4638
|
summaryEl.textContent = "";
|
|
4609
4639
|
summaryEl.hidden = true;
|
|
4610
4640
|
const cfg = opts.summaryRow;
|
|
@@ -4801,9 +4831,9 @@ function createTable(el, opts) {
|
|
|
4801
4831
|
}
|
|
4802
4832
|
}
|
|
4803
4833
|
let currentTotal = 0;
|
|
4804
|
-
function visibleRange(total) {
|
|
4834
|
+
function visibleRange(total, avail) {
|
|
4805
4835
|
if (!virtual) {
|
|
4806
|
-
if (opts.remote || opts.pagination === false) return [0, total];
|
|
4836
|
+
if (opts.remote || opts.pagination === false) return [0, Math.min(total, avail)];
|
|
4807
4837
|
const start2 = (page - 1) * pageSize;
|
|
4808
4838
|
return [start2, Math.min(total, start2 + pageSize)];
|
|
4809
4839
|
}
|
|
@@ -4818,6 +4848,10 @@ function createTable(el, opts) {
|
|
|
4818
4848
|
const rows = pipeline();
|
|
4819
4849
|
const entries = groupFn ? flatEntries() : null;
|
|
4820
4850
|
const total = entries ? entries.length : opts.remote ? opts.totalCount ?? data.length : rows.length;
|
|
4851
|
+
if (!opts.remote && opts.pagination !== false) {
|
|
4852
|
+
const pages = Math.max(1, Math.ceil(total / pageSize));
|
|
4853
|
+
if (page > pages) page = pages;
|
|
4854
|
+
}
|
|
4821
4855
|
navTotal = entries ? entries.length : rows.length;
|
|
4822
4856
|
currentTotal = total;
|
|
4823
4857
|
const keepTop = virtual ? scroll.scrollTop : 0;
|
|
@@ -4834,7 +4868,7 @@ function createTable(el, opts) {
|
|
|
4834
4868
|
body.appendChild(sk);
|
|
4835
4869
|
}
|
|
4836
4870
|
renderFoot(total, rows.length);
|
|
4837
|
-
|
|
4871
|
+
renderSummary2();
|
|
4838
4872
|
return;
|
|
4839
4873
|
}
|
|
4840
4874
|
if (total === 0) {
|
|
@@ -4843,10 +4877,10 @@ function createTable(el, opts) {
|
|
|
4843
4877
|
empty.textContent = opts.emptyLabel ?? t("datatable.empty");
|
|
4844
4878
|
body.appendChild(empty);
|
|
4845
4879
|
renderFoot(total, rows.length);
|
|
4846
|
-
|
|
4880
|
+
renderSummary2();
|
|
4847
4881
|
return;
|
|
4848
4882
|
}
|
|
4849
|
-
const [start, end] = visibleRange(total);
|
|
4883
|
+
const [start, end] = visibleRange(total, rows.length);
|
|
4850
4884
|
const frag = document.createDocumentFragment();
|
|
4851
4885
|
const offsets = frozenOffsets();
|
|
4852
4886
|
if (virtual && start > 0) {
|
|
@@ -4880,7 +4914,7 @@ function createTable(el, opts) {
|
|
|
4880
4914
|
if (virtual) scroll.scrollTop = keepTop;
|
|
4881
4915
|
syncNavFocus();
|
|
4882
4916
|
renderFoot(total, rows.length);
|
|
4883
|
-
|
|
4917
|
+
renderSummary2();
|
|
4884
4918
|
}
|
|
4885
4919
|
function renderFoot(total, dataCount = total) {
|
|
4886
4920
|
foot.textContent = "";
|
|
@@ -4909,11 +4943,21 @@ function createTable(el, opts) {
|
|
|
4909
4943
|
const pages = Math.ceil(total / pageSize);
|
|
4910
4944
|
const pager = document.createElement("nav");
|
|
4911
4945
|
pager.className = "icen-dt-pager";
|
|
4912
|
-
|
|
4946
|
+
{
|
|
4947
|
+
const base = t("datatable.paginationAria");
|
|
4948
|
+
const used = /* @__PURE__ */ new Set();
|
|
4949
|
+
for (const el2 of document.querySelectorAll(".icen-dt-pager[aria-label]")) {
|
|
4950
|
+
if (!root.contains(el2)) used.add(el2.getAttribute("aria-label") ?? "");
|
|
4951
|
+
}
|
|
4952
|
+
let label = base, n = 2;
|
|
4953
|
+
while (used.has(label)) label = `${base} ${n++}`;
|
|
4954
|
+
pager.setAttribute("aria-label", label);
|
|
4955
|
+
}
|
|
4913
4956
|
const sizeSel = document.createElement("select");
|
|
4914
4957
|
sizeSel.className = "icen-dt-page-size";
|
|
4915
4958
|
sizeSel.setAttribute("aria-label", t("datatable.pageSizeAria"));
|
|
4916
|
-
|
|
4959
|
+
const sizeOptions = pageSizes.includes(pageSize) ? pageSizes : [...pageSizes, pageSize].sort((a, b) => a - b);
|
|
4960
|
+
for (const s of sizeOptions) {
|
|
4917
4961
|
const o = document.createElement("option");
|
|
4918
4962
|
o.value = String(s);
|
|
4919
4963
|
o.textContent = t("pagination.pageSize", { p0: s });
|
|
@@ -5005,6 +5049,7 @@ function createTable(el, opts) {
|
|
|
5005
5049
|
if (destroyed) return;
|
|
5006
5050
|
renderHead();
|
|
5007
5051
|
renderBody();
|
|
5052
|
+
syncFrozenWidths();
|
|
5008
5053
|
}
|
|
5009
5054
|
rekey();
|
|
5010
5055
|
renderAll();
|
|
@@ -5025,12 +5070,52 @@ function createTable(el, opts) {
|
|
|
5025
5070
|
opts.onPageChange?.(page, pageSize);
|
|
5026
5071
|
renderAll();
|
|
5027
5072
|
}
|
|
5073
|
+
function hasFluidFrozen() {
|
|
5074
|
+
return cols().some((c) => frozenSide(c) != null && !/^(\d+(?:\.\d+)?)px/.test(colWidths.get(c.key) ?? c.width ?? ""));
|
|
5075
|
+
}
|
|
5076
|
+
function syncFrozenWidths() {
|
|
5077
|
+
if (!hasFluidFrozen()) return;
|
|
5078
|
+
const visible = cols();
|
|
5079
|
+
const headCells = Array.from(headRow.children);
|
|
5080
|
+
const lead = headCells.length - visible.length;
|
|
5081
|
+
const left = /* @__PURE__ */ new Map();
|
|
5082
|
+
const right = /* @__PURE__ */ new Map();
|
|
5083
|
+
let acc = headCells.slice(0, lead).reduce((s, c) => s + c.offsetWidth, 0);
|
|
5084
|
+
for (let i = 0; i < visible.length; i++) {
|
|
5085
|
+
const c = visible[i];
|
|
5086
|
+
if (frozenSide(c) === "left") left.set(c.key, acc);
|
|
5087
|
+
acc += headCells[lead + i]?.offsetWidth ?? 0;
|
|
5088
|
+
}
|
|
5089
|
+
let accR = 0;
|
|
5090
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
5091
|
+
const c = visible[i];
|
|
5092
|
+
if (frozenSide(c) === "right") right.set(c.key, accR);
|
|
5093
|
+
accR += headCells[lead + i]?.offsetWidth ?? 0;
|
|
5094
|
+
}
|
|
5095
|
+
const rows = [headRow, ...summaryEl ? [summaryEl] : [], ...Array.from(body.querySelectorAll(".icen-dt-row"))];
|
|
5096
|
+
for (const row of rows) {
|
|
5097
|
+
const cells = Array.from(row.children);
|
|
5098
|
+
if (cells.length !== headCells.length) continue;
|
|
5099
|
+
cells.forEach((cell, i) => {
|
|
5100
|
+
const col = i >= lead ? visible[i - lead] : void 0;
|
|
5101
|
+
if (!col) return;
|
|
5102
|
+
const l = left.get(col.key);
|
|
5103
|
+
if (l != null && cell.classList.contains("icen-dt-cell--frozen")) cell.style.left = `${l}px`;
|
|
5104
|
+
const r = right.get(col.key);
|
|
5105
|
+
if (r != null && cell.classList.contains("icen-dt-cell--frozen-right")) cell.style.right = `${r}px`;
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5028
5109
|
return {
|
|
5029
5110
|
setPage,
|
|
5030
5111
|
setPageSize,
|
|
5031
5112
|
setData(next) {
|
|
5032
5113
|
if (editing) cancelEdit(true);
|
|
5033
5114
|
data = next;
|
|
5115
|
+
if (typeof opts.rowKey !== "function" && typeof opts.rowKey !== "string" && selected.size > 0) {
|
|
5116
|
+
selected.clear();
|
|
5117
|
+
emitSelection();
|
|
5118
|
+
}
|
|
5034
5119
|
rekey();
|
|
5035
5120
|
invalidate();
|
|
5036
5121
|
page = 1;
|
|
@@ -5107,6 +5192,823 @@ function createTable(el, opts) {
|
|
|
5107
5192
|
};
|
|
5108
5193
|
}
|
|
5109
5194
|
|
|
5195
|
+
// src/behaviors/form-validate.ts
|
|
5196
|
+
var DEFAULT_MESSAGES = {
|
|
5197
|
+
required: t("formValidate.required"),
|
|
5198
|
+
email: t("formValidate.email"),
|
|
5199
|
+
url: t("formValidate.url"),
|
|
5200
|
+
number: t("formValidate.number"),
|
|
5201
|
+
minlength: t("formValidate.minlength"),
|
|
5202
|
+
maxlength: t("formValidate.maxlength"),
|
|
5203
|
+
pattern: t("formValidate.pattern"),
|
|
5204
|
+
min: t("formValidate.min"),
|
|
5205
|
+
max: t("formValidate.max"),
|
|
5206
|
+
step: t("formValidate.step"),
|
|
5207
|
+
matches: t("formValidate.matches")
|
|
5208
|
+
};
|
|
5209
|
+
var messageTable = { ...DEFAULT_MESSAGES };
|
|
5210
|
+
function formatMessage(template, tokens) {
|
|
5211
|
+
return template.replace(/\{(\w+)\}/g, (m, key) => key in tokens ? String(tokens[key]) : m);
|
|
5212
|
+
}
|
|
5213
|
+
var customValidators = /* @__PURE__ */ new Map();
|
|
5214
|
+
function attrNumber(el, name) {
|
|
5215
|
+
const raw = el.getAttribute(name);
|
|
5216
|
+
if (raw == null || raw === "" || !Number.isFinite(Number(raw))) return null;
|
|
5217
|
+
return Number(raw);
|
|
5218
|
+
}
|
|
5219
|
+
function resolveFieldRules(field) {
|
|
5220
|
+
const rules = [];
|
|
5221
|
+
const type = (field.getAttribute("type") ?? "").toLowerCase();
|
|
5222
|
+
if (field.hasAttribute("required")) rules.push({ rule: "required" });
|
|
5223
|
+
if (type === "email" || type === "url") rules.push({ rule: type });
|
|
5224
|
+
if (type === "number" || type === "range") rules.push({ rule: "number" });
|
|
5225
|
+
const minlength = attrNumber(field, "minlength");
|
|
5226
|
+
if (minlength != null) rules.push({ rule: "minlength", attr: String(minlength) });
|
|
5227
|
+
const maxlength = attrNumber(field, "maxlength");
|
|
5228
|
+
if (maxlength != null) rules.push({ rule: "maxlength", attr: String(maxlength) });
|
|
5229
|
+
const pattern = field.getAttribute("pattern");
|
|
5230
|
+
if (pattern) rules.push({ rule: "pattern", attr: pattern });
|
|
5231
|
+
const min = field.getAttribute("min");
|
|
5232
|
+
if (min != null && min !== "") rules.push({ rule: "min", attr: min });
|
|
5233
|
+
const max = field.getAttribute("max");
|
|
5234
|
+
if (max != null && max !== "") rules.push({ rule: "max", attr: max });
|
|
5235
|
+
const step = field.getAttribute("step");
|
|
5236
|
+
if (step != null && step !== "" && step !== "any") rules.push({ rule: "step", attr: step });
|
|
5237
|
+
const extra = field.getAttribute("data-icen-validate") ?? "";
|
|
5238
|
+
for (const token of extra.trim().split(/\s+/)) {
|
|
5239
|
+
if (!token) continue;
|
|
5240
|
+
const idx = token.indexOf(":");
|
|
5241
|
+
const rule = idx === -1 ? token : token.slice(0, idx);
|
|
5242
|
+
const arg = idx === -1 ? void 0 : token.slice(idx + 1) || void 0;
|
|
5243
|
+
if (!rule || rule === "matches" && !arg) continue;
|
|
5244
|
+
rules.push({ rule, arg });
|
|
5245
|
+
}
|
|
5246
|
+
return rules;
|
|
5247
|
+
}
|
|
5248
|
+
function fieldValue(field) {
|
|
5249
|
+
if (field instanceof HTMLInputElement && (field.type === "checkbox" || field.type === "radio")) {
|
|
5250
|
+
return field.checked ? field.value || "on" : "";
|
|
5251
|
+
}
|
|
5252
|
+
return field.value;
|
|
5253
|
+
}
|
|
5254
|
+
function fieldLabel(field, fallback = "") {
|
|
5255
|
+
if (!field) return fallback;
|
|
5256
|
+
const wrap = field.closest(".icen-field, .icen-form-field");
|
|
5257
|
+
const labelEl = wrap?.querySelector(".icen-field-label, .icen-form-label, label");
|
|
5258
|
+
const text = labelEl?.textContent?.replace(/\s+/g, " ").trim();
|
|
5259
|
+
return text || field.getAttribute("aria-label") || field.getAttribute("placeholder") || field.getAttribute("name") || fallback;
|
|
5260
|
+
}
|
|
5261
|
+
function ruleMessage(field, rule, tokens) {
|
|
5262
|
+
const custom2 = field?.getAttribute(`data-icen-msg-${rule}`);
|
|
5263
|
+
const template = custom2 || messageTable[rule] || DEFAULT_MESSAGES[rule] || t("formValidate.invalid");
|
|
5264
|
+
return formatMessage(template, tokens);
|
|
5265
|
+
}
|
|
5266
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5267
|
+
var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "range"]);
|
|
5268
|
+
function compareValue(field, value, attr) {
|
|
5269
|
+
const numericAttr = Number.isFinite(Number(attr));
|
|
5270
|
+
const type = field?.getAttribute("type")?.toLowerCase() ?? "";
|
|
5271
|
+
if (numericAttr && (NUMBER_TYPES.has(type) || Number.isFinite(Number(value)))) {
|
|
5272
|
+
if (!Number.isFinite(Number(value))) return null;
|
|
5273
|
+
return Number(value) - Number(attr);
|
|
5274
|
+
}
|
|
5275
|
+
if (!numericAttr) return value < attr ? -1 : value > attr ? 1 : 0;
|
|
5276
|
+
return null;
|
|
5277
|
+
}
|
|
5278
|
+
async function applyRule(value, rule, ctx) {
|
|
5279
|
+
const field = ctx.field;
|
|
5280
|
+
const tokens = {
|
|
5281
|
+
min: rule.attr ?? "",
|
|
5282
|
+
max: rule.attr ?? "",
|
|
5283
|
+
step: rule.attr ?? "",
|
|
5284
|
+
length: value.length,
|
|
5285
|
+
label: fieldLabel(field, rule.arg ?? rule.rule)
|
|
5286
|
+
};
|
|
5287
|
+
switch (rule.rule) {
|
|
5288
|
+
case "required": {
|
|
5289
|
+
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
5290
|
+
return field.checked ? true : ruleMessage(field, "required", tokens);
|
|
5291
|
+
}
|
|
5292
|
+
if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
5293
|
+
const form = ctx.form ?? field.form;
|
|
5294
|
+
const checked = form ? Array.from(form.elements).some(
|
|
5295
|
+
(el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === field.name && el.checked
|
|
5296
|
+
) : field.checked;
|
|
5297
|
+
return checked ? true : ruleMessage(field, "required", tokens);
|
|
5298
|
+
}
|
|
5299
|
+
return value.trim() !== "" ? true : ruleMessage(field, "required", tokens);
|
|
5300
|
+
}
|
|
5301
|
+
case "email":
|
|
5302
|
+
return value === "" || EMAIL_RE.test(value) ? true : ruleMessage(field, "email", tokens);
|
|
5303
|
+
case "url": {
|
|
5304
|
+
if (value === "") return true;
|
|
5305
|
+
try {
|
|
5306
|
+
new URL(value);
|
|
5307
|
+
return true;
|
|
5308
|
+
} catch {
|
|
5309
|
+
return ruleMessage(field, "url", tokens);
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
case "number":
|
|
5313
|
+
return value === "" || Number.isFinite(Number(value)) ? true : ruleMessage(field, "number", tokens);
|
|
5314
|
+
case "minlength": {
|
|
5315
|
+
if (value === "") return true;
|
|
5316
|
+
const min = Number(rule.attr);
|
|
5317
|
+
return value.length >= min ? true : ruleMessage(field, "minlength", tokens);
|
|
5318
|
+
}
|
|
5319
|
+
case "maxlength": {
|
|
5320
|
+
if (value === "") return true;
|
|
5321
|
+
const max = Number(rule.attr);
|
|
5322
|
+
return value.length <= max ? true : ruleMessage(field, "maxlength", tokens);
|
|
5323
|
+
}
|
|
5324
|
+
case "pattern": {
|
|
5325
|
+
if (value === "") return true;
|
|
5326
|
+
try {
|
|
5327
|
+
return new RegExp(`^(?:${rule.attr})$`).test(value) ? true : ruleMessage(field, "pattern", tokens);
|
|
5328
|
+
} catch {
|
|
5329
|
+
return true;
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
case "min": {
|
|
5333
|
+
if (value === "") return true;
|
|
5334
|
+
const cmp = compareValue(field, value, rule.attr ?? "");
|
|
5335
|
+
return cmp == null || cmp >= 0 ? true : ruleMessage(field, "min", tokens);
|
|
5336
|
+
}
|
|
5337
|
+
case "max": {
|
|
5338
|
+
if (value === "") return true;
|
|
5339
|
+
const cmp = compareValue(field, value, rule.attr ?? "");
|
|
5340
|
+
return cmp == null || cmp <= 0 ? true : ruleMessage(field, "max", tokens);
|
|
5341
|
+
}
|
|
5342
|
+
case "step": {
|
|
5343
|
+
if (value === "") return true;
|
|
5344
|
+
const step = Number(rule.attr);
|
|
5345
|
+
if (!Number.isFinite(step) || step <= 0 || !Number.isFinite(Number(value))) return true;
|
|
5346
|
+
const minAttr = field?.getAttribute("min");
|
|
5347
|
+
const base = minAttr != null && minAttr !== "" && Number.isFinite(Number(minAttr)) ? Number(minAttr) : 0;
|
|
5348
|
+
const rem = Math.abs((Number(value) - base) % step);
|
|
5349
|
+
return rem < 1e-9 || step - rem < 1e-9 ? true : ruleMessage(field, "step", tokens);
|
|
5350
|
+
}
|
|
5351
|
+
case "matches": {
|
|
5352
|
+
const form = ctx.form ?? field?.form ?? null;
|
|
5353
|
+
const other = form && rule.arg ? form.elements.namedItem(rule.arg) : null;
|
|
5354
|
+
const otherEl = other instanceof Element ? other : null;
|
|
5355
|
+
if (!otherEl) return true;
|
|
5356
|
+
return value === otherEl.value ? true : ruleMessage(field, "matches", tokens);
|
|
5357
|
+
}
|
|
5358
|
+
default: {
|
|
5359
|
+
const fn = customValidators.get(rule.rule);
|
|
5360
|
+
if (!fn || !field) return true;
|
|
5361
|
+
const result = await fn(value, field, ctx.form ?? field.form ?? null);
|
|
5362
|
+
return result === true ? true : typeof result === "string" ? result : true;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
async function evaluateRules(value, rules, ctx = {}) {
|
|
5367
|
+
for (const rule of rules) {
|
|
5368
|
+
const fail = await applyRule(value, rule, ctx);
|
|
5369
|
+
if (fail !== true) return fail;
|
|
5370
|
+
}
|
|
5371
|
+
return true;
|
|
5372
|
+
}
|
|
5373
|
+
var DEFAULT_DEBOUNCE = 160;
|
|
5374
|
+
var uidSeq = 0;
|
|
5375
|
+
var liveValidators = /* @__PURE__ */ new WeakMap();
|
|
5376
|
+
var composingFlags = /* @__PURE__ */ new WeakMap();
|
|
5377
|
+
var compositionTracked = /* @__PURE__ */ new WeakSet();
|
|
5378
|
+
function trackComposition(el) {
|
|
5379
|
+
if (compositionTracked.has(el)) return;
|
|
5380
|
+
compositionTracked.add(el);
|
|
5381
|
+
el.addEventListener("compositionstart", () => composingFlags.set(el, true));
|
|
5382
|
+
el.addEventListener("compositionend", () => composingFlags.set(el, false));
|
|
5383
|
+
}
|
|
5384
|
+
function isComposing(el) {
|
|
5385
|
+
return composingFlags.get(el) === true;
|
|
5386
|
+
}
|
|
5387
|
+
function collectFormFields(form) {
|
|
5388
|
+
const out = [];
|
|
5389
|
+
for (const el of Array.from(form.elements)) {
|
|
5390
|
+
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
|
|
5391
|
+
continue;
|
|
5392
|
+
}
|
|
5393
|
+
if (el.disabled || !el.name) continue;
|
|
5394
|
+
if (el instanceof HTMLInputElement) {
|
|
5395
|
+
const type = (el.getAttribute("type") ?? "").toLowerCase();
|
|
5396
|
+
if (["submit", "button", "reset", "image", "file"].includes(type)) continue;
|
|
5397
|
+
}
|
|
5398
|
+
out.push(el);
|
|
5399
|
+
}
|
|
5400
|
+
return out;
|
|
5401
|
+
}
|
|
5402
|
+
function findFormField(form, name) {
|
|
5403
|
+
const item = form.elements.namedItem(name);
|
|
5404
|
+
if (item instanceof Element && (item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement || item instanceof HTMLSelectElement)) {
|
|
5405
|
+
return item;
|
|
5406
|
+
}
|
|
5407
|
+
if (item instanceof RadioNodeList) {
|
|
5408
|
+
for (const el of Array.from(item)) {
|
|
5409
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
5410
|
+
return el;
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
return null;
|
|
5415
|
+
}
|
|
5416
|
+
function fieldWrap(field) {
|
|
5417
|
+
return closestSelfOrAncestor(field, ".icen-field, .icen-form-field");
|
|
5418
|
+
}
|
|
5419
|
+
function closestSelfOrAncestor(el, selector) {
|
|
5420
|
+
return el.matches(selector) ? el : el.closest(selector);
|
|
5421
|
+
}
|
|
5422
|
+
function ensureControlId(field) {
|
|
5423
|
+
if (!field.id) field.id = `icen-fv-${++uidSeq}`;
|
|
5424
|
+
return field.id;
|
|
5425
|
+
}
|
|
5426
|
+
function findErrorEl(wrap) {
|
|
5427
|
+
for (const el of Array.from(wrap.children)) {
|
|
5428
|
+
if (el instanceof HTMLElement && el.classList.contains("icen-field-error")) return el;
|
|
5429
|
+
}
|
|
5430
|
+
return null;
|
|
5431
|
+
}
|
|
5432
|
+
function setValidating(field, on) {
|
|
5433
|
+
const wrap = fieldWrap(field);
|
|
5434
|
+
if (wrap) wrap.classList.toggle("is-validating", on);
|
|
5435
|
+
if (on) field.setAttribute("aria-busy", "true");
|
|
5436
|
+
else field.removeAttribute("aria-busy");
|
|
5437
|
+
}
|
|
5438
|
+
function presentField(field, outcome, showValid) {
|
|
5439
|
+
const wrap = fieldWrap(field);
|
|
5440
|
+
setValidating(field, false);
|
|
5441
|
+
if (!wrap) {
|
|
5442
|
+
if (!outcome.ok) field.setAttribute("aria-invalid", "true");
|
|
5443
|
+
else field.removeAttribute("aria-invalid");
|
|
5444
|
+
return;
|
|
5445
|
+
}
|
|
5446
|
+
if (!outcome.ok) {
|
|
5447
|
+
wrap.classList.add("is-error");
|
|
5448
|
+
wrap.setAttribute("data-icen-invalid", "true");
|
|
5449
|
+
wrap.classList.remove("is-valid");
|
|
5450
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5451
|
+
let errEl2 = findErrorEl(wrap);
|
|
5452
|
+
if (!errEl2) {
|
|
5453
|
+
errEl2 = document.createElement("p");
|
|
5454
|
+
errEl2.className = "icen-field-error";
|
|
5455
|
+
errEl2.id = `icen-fv-err-${++uidSeq}`;
|
|
5456
|
+
errEl2.setAttribute("role", "alert");
|
|
5457
|
+
errEl2.setAttribute("data-icen-fv-auto", "");
|
|
5458
|
+
wrap.appendChild(errEl2);
|
|
5459
|
+
}
|
|
5460
|
+
errEl2.hidden = false;
|
|
5461
|
+
errEl2.textContent = outcome.message ?? "";
|
|
5462
|
+
field.setAttribute("aria-invalid", "true");
|
|
5463
|
+
const described = new Set((field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean));
|
|
5464
|
+
described.add(errEl2.id);
|
|
5465
|
+
field.setAttribute("aria-describedby", Array.from(described).join(" "));
|
|
5466
|
+
return;
|
|
5467
|
+
}
|
|
5468
|
+
wrap.classList.remove("is-error");
|
|
5469
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5470
|
+
field.removeAttribute("aria-invalid");
|
|
5471
|
+
const errEl = findErrorEl(wrap);
|
|
5472
|
+
if (errEl) {
|
|
5473
|
+
const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => id !== errEl.id);
|
|
5474
|
+
if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
|
|
5475
|
+
else field.removeAttribute("aria-describedby");
|
|
5476
|
+
if (errEl.hasAttribute("data-icen-fv-auto")) errEl.remove();
|
|
5477
|
+
else {
|
|
5478
|
+
errEl.textContent = "";
|
|
5479
|
+
errEl.hidden = true;
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
if (showValid) {
|
|
5483
|
+
wrap.classList.add("is-valid");
|
|
5484
|
+
wrap.setAttribute("data-icen-valid", "true");
|
|
5485
|
+
} else {
|
|
5486
|
+
wrap.classList.remove("is-valid");
|
|
5487
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
function clearFieldPresentation(field) {
|
|
5491
|
+
const wrap = fieldWrap(field);
|
|
5492
|
+
if (wrap) {
|
|
5493
|
+
wrap.classList.remove("is-error", "is-valid", "is-validating");
|
|
5494
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5495
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5496
|
+
const errEl = findErrorEl(wrap);
|
|
5497
|
+
if (errEl?.hasAttribute("data-icen-fv-auto")) errEl.remove();
|
|
5498
|
+
else if (errEl) {
|
|
5499
|
+
errEl.textContent = "";
|
|
5500
|
+
errEl.hidden = true;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
field.removeAttribute("aria-invalid");
|
|
5504
|
+
field.removeAttribute("aria-busy");
|
|
5505
|
+
const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => !id.startsWith("icen-fv-err-"));
|
|
5506
|
+
if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
|
|
5507
|
+
else field.removeAttribute("aria-describedby");
|
|
5508
|
+
}
|
|
5509
|
+
function focusField(field) {
|
|
5510
|
+
try {
|
|
5511
|
+
field.focus();
|
|
5512
|
+
} catch {
|
|
5513
|
+
}
|
|
5514
|
+
if (typeof field.scrollIntoView === "function") {
|
|
5515
|
+
try {
|
|
5516
|
+
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5517
|
+
field.scrollIntoView({ block: "center", behavior: reduced ? "auto" : "smooth" });
|
|
5518
|
+
} catch {
|
|
5519
|
+
try {
|
|
5520
|
+
field.scrollIntoView();
|
|
5521
|
+
} catch {
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
function renderSummary(form, errorsObj) {
|
|
5527
|
+
if (!form.hasAttribute("data-icen-validate-summary")) return;
|
|
5528
|
+
const names = Object.keys(errorsObj);
|
|
5529
|
+
const existing = form.querySelector(":scope > .icen-form-validate-summary");
|
|
5530
|
+
if (names.length === 0) {
|
|
5531
|
+
existing?.remove();
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
let box = existing;
|
|
5535
|
+
if (!box) {
|
|
5536
|
+
box = document.createElement("div");
|
|
5537
|
+
box.className = "icen-form-validate-summary";
|
|
5538
|
+
box.setAttribute("role", "alert");
|
|
5539
|
+
box.setAttribute("data-icen-fv-auto", "");
|
|
5540
|
+
box.addEventListener("click", (e) => {
|
|
5541
|
+
const a = e.target instanceof Element ? closestSelfOrAncestor(e.target, "a[data-icen-fv-name]") : null;
|
|
5542
|
+
if (!(a instanceof HTMLAnchorElement)) return;
|
|
5543
|
+
e.preventDefault();
|
|
5544
|
+
const field = findFormField(form, a.getAttribute("data-icen-fv-name") ?? "");
|
|
5545
|
+
if (field) focusField(field);
|
|
5546
|
+
});
|
|
5547
|
+
form.prepend(box);
|
|
5548
|
+
}
|
|
5549
|
+
box.textContent = "";
|
|
5550
|
+
const title = document.createElement("p");
|
|
5551
|
+
title.className = "icen-form-validate-summary-title";
|
|
5552
|
+
title.textContent = t("formValidate.summary", { p0: names.length });
|
|
5553
|
+
const list = document.createElement("ul");
|
|
5554
|
+
list.className = "icen-form-validate-summary-list";
|
|
5555
|
+
for (const name of names) {
|
|
5556
|
+
const field = findFormField(form, name);
|
|
5557
|
+
if (!field) continue;
|
|
5558
|
+
const li = document.createElement("li");
|
|
5559
|
+
const a = document.createElement("a");
|
|
5560
|
+
a.className = "icen-form-validate-summary-item";
|
|
5561
|
+
a.href = `#${ensureControlId(field)}`;
|
|
5562
|
+
a.setAttribute("data-icen-fv-name", name);
|
|
5563
|
+
a.textContent = `${fieldLabel(field, name)}\uFF1A${errorsObj[name]}`;
|
|
5564
|
+
li.appendChild(a);
|
|
5565
|
+
list.appendChild(li);
|
|
5566
|
+
}
|
|
5567
|
+
box.append(title, list);
|
|
5568
|
+
}
|
|
5569
|
+
function createFormValidator(form, opts = {}) {
|
|
5570
|
+
if (typeof document === "undefined") {
|
|
5571
|
+
const stub = () => void 0;
|
|
5572
|
+
return {
|
|
5573
|
+
form,
|
|
5574
|
+
validate: async () => true,
|
|
5575
|
+
validateField: async () => true,
|
|
5576
|
+
reset: stub,
|
|
5577
|
+
errors: () => ({}),
|
|
5578
|
+
destroy: stub
|
|
5579
|
+
};
|
|
5580
|
+
}
|
|
5581
|
+
const existing = liveValidators.get(form);
|
|
5582
|
+
if (existing) return existing;
|
|
5583
|
+
const state = {
|
|
5584
|
+
errors: /* @__PURE__ */ new Map(),
|
|
5585
|
+
touched: /* @__PURE__ */ new Set(),
|
|
5586
|
+
submitted: false,
|
|
5587
|
+
seq: /* @__PURE__ */ new Map(),
|
|
5588
|
+
debounceTimers: /* @__PURE__ */ new Map()
|
|
5589
|
+
};
|
|
5590
|
+
let pointerDown = false;
|
|
5591
|
+
const pendingPresent = /* @__PURE__ */ new Map();
|
|
5592
|
+
let pendingSummary = null;
|
|
5593
|
+
let flushScheduled = false;
|
|
5594
|
+
const showValidFor = (field) => field.hasAttribute("data-icen-validate-show-valid") || form.hasAttribute("data-icen-validate-show-valid");
|
|
5595
|
+
async function checkField(field) {
|
|
5596
|
+
const name = field.name;
|
|
5597
|
+
const rules = resolveFieldRules(field);
|
|
5598
|
+
if (rules.length === 0) return true;
|
|
5599
|
+
trackComposition(field);
|
|
5600
|
+
const seq = (state.seq.get(name) ?? 0) + 1;
|
|
5601
|
+
state.seq.set(name, seq);
|
|
5602
|
+
const hasAsync = rules.some((r) => customValidators.has(r.rule));
|
|
5603
|
+
if (hasAsync) setValidating(field, true);
|
|
5604
|
+
const result = await evaluateRules(fieldValue(field), rules, { field, form });
|
|
5605
|
+
if (state.seq.get(name) !== seq) {
|
|
5606
|
+
if (hasAsync) setValidating(field, false);
|
|
5607
|
+
return !state.errors.has(name);
|
|
5608
|
+
}
|
|
5609
|
+
if (hasAsync) setValidating(field, false);
|
|
5610
|
+
const outcome = result === true ? { ok: true } : { ok: false, message: result };
|
|
5611
|
+
if (outcome.ok) state.errors.delete(name);
|
|
5612
|
+
else state.errors.set(name, outcome.message ?? "");
|
|
5613
|
+
presentFieldGuarded(field, outcome, showValidFor(field));
|
|
5614
|
+
if (form.querySelector(":scope > .icen-form-validate-summary")) {
|
|
5615
|
+
renderSummaryGuarded(errors());
|
|
5616
|
+
}
|
|
5617
|
+
emitIcen(
|
|
5618
|
+
field,
|
|
5619
|
+
"icen:form-field-validate",
|
|
5620
|
+
outcome.ok ? { name, valid: true } : { name, valid: false, message: outcome.message }
|
|
5621
|
+
);
|
|
5622
|
+
return outcome.ok;
|
|
5623
|
+
}
|
|
5624
|
+
function debouncedCheck(field) {
|
|
5625
|
+
const name = field.name;
|
|
5626
|
+
const timer = state.debounceTimers.get(name);
|
|
5627
|
+
if (timer) clearTimeout(timer);
|
|
5628
|
+
const delay = opts.strategy?.debounceMs ?? DEFAULT_DEBOUNCE;
|
|
5629
|
+
state.debounceTimers.set(
|
|
5630
|
+
name,
|
|
5631
|
+
setTimeout(() => {
|
|
5632
|
+
state.debounceTimers.delete(name);
|
|
5633
|
+
void checkField(field);
|
|
5634
|
+
}, delay)
|
|
5635
|
+
);
|
|
5636
|
+
}
|
|
5637
|
+
function presentFieldPressed(field, outcome, showValid) {
|
|
5638
|
+
pendingPresent.set(field, { outcome, showValid });
|
|
5639
|
+
const wrap = fieldWrap(field);
|
|
5640
|
+
setValidating(field, false);
|
|
5641
|
+
if (!wrap) {
|
|
5642
|
+
if (!outcome.ok) field.setAttribute("aria-invalid", "true");
|
|
5643
|
+
else field.removeAttribute("aria-invalid");
|
|
5644
|
+
return;
|
|
5645
|
+
}
|
|
5646
|
+
if (!outcome.ok) {
|
|
5647
|
+
wrap.classList.add("is-error");
|
|
5648
|
+
wrap.setAttribute("data-icen-invalid", "true");
|
|
5649
|
+
wrap.classList.remove("is-valid");
|
|
5650
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5651
|
+
field.setAttribute("aria-invalid", "true");
|
|
5652
|
+
const errEl = findErrorEl(wrap);
|
|
5653
|
+
if (errEl && !errEl.hidden) errEl.textContent = outcome.message ?? "";
|
|
5654
|
+
return;
|
|
5655
|
+
}
|
|
5656
|
+
wrap.classList.remove("is-error");
|
|
5657
|
+
wrap.removeAttribute("data-icen-invalid");
|
|
5658
|
+
field.removeAttribute("aria-invalid");
|
|
5659
|
+
if (showValid) {
|
|
5660
|
+
wrap.classList.add("is-valid");
|
|
5661
|
+
wrap.setAttribute("data-icen-valid", "true");
|
|
5662
|
+
} else {
|
|
5663
|
+
wrap.classList.remove("is-valid");
|
|
5664
|
+
wrap.removeAttribute("data-icen-valid");
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
function presentFieldGuarded(field, outcome, showValid) {
|
|
5668
|
+
if (pointerDown) presentFieldPressed(field, outcome, showValid);
|
|
5669
|
+
else presentField(field, outcome, showValid);
|
|
5670
|
+
}
|
|
5671
|
+
function renderSummaryGuarded(errorsObj) {
|
|
5672
|
+
if (pointerDown) pendingSummary = errorsObj;
|
|
5673
|
+
else renderSummary(form, errorsObj);
|
|
5674
|
+
}
|
|
5675
|
+
function flushDeferredPresentation() {
|
|
5676
|
+
flushScheduled = false;
|
|
5677
|
+
if (destroyed) return;
|
|
5678
|
+
const items = Array.from(pendingPresent.entries());
|
|
5679
|
+
pendingPresent.clear();
|
|
5680
|
+
const summary = pendingSummary;
|
|
5681
|
+
pendingSummary = null;
|
|
5682
|
+
for (const [field, it] of items) presentField(field, it.outcome, it.showValid);
|
|
5683
|
+
if (summary) renderSummary(form, summary);
|
|
5684
|
+
}
|
|
5685
|
+
function scheduleFlush() {
|
|
5686
|
+
if (flushScheduled) return;
|
|
5687
|
+
flushScheduled = true;
|
|
5688
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flushDeferredPresentation);
|
|
5689
|
+
else setTimeout(flushDeferredPresentation, 0);
|
|
5690
|
+
}
|
|
5691
|
+
function resolveEventField(target) {
|
|
5692
|
+
if (!(target instanceof Element)) return null;
|
|
5693
|
+
const el = closestSelfOrAncestor(target, "input, textarea, select");
|
|
5694
|
+
if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
|
|
5695
|
+
return null;
|
|
5696
|
+
}
|
|
5697
|
+
if (el.form !== form || el.disabled || !el.name) return null;
|
|
5698
|
+
if (el instanceof HTMLInputElement) {
|
|
5699
|
+
const type = (el.getAttribute("type") ?? "").toLowerCase();
|
|
5700
|
+
if (["submit", "button", "reset", "image", "file"].includes(type)) return null;
|
|
5701
|
+
}
|
|
5702
|
+
return el;
|
|
5703
|
+
}
|
|
5704
|
+
const onSubmit = (e) => {
|
|
5705
|
+
e.preventDefault();
|
|
5706
|
+
void validate();
|
|
5707
|
+
};
|
|
5708
|
+
const onFocusOut = (e) => {
|
|
5709
|
+
const field = resolveEventField(e.target);
|
|
5710
|
+
if (!field) return;
|
|
5711
|
+
state.touched.add(field.name);
|
|
5712
|
+
const mode = opts.strategy?.on;
|
|
5713
|
+
if (mode === "blur") void checkField(field);
|
|
5714
|
+
else if (!mode && state.submitted) void checkField(field);
|
|
5715
|
+
};
|
|
5716
|
+
const onInput = (e) => {
|
|
5717
|
+
const field = resolveEventField(e.target);
|
|
5718
|
+
if (!field) return;
|
|
5719
|
+
trackComposition(field);
|
|
5720
|
+
if (isComposing(field)) return;
|
|
5721
|
+
state.touched.add(field.name);
|
|
5722
|
+
const mode = opts.strategy?.on;
|
|
5723
|
+
if (mode === "input") debouncedCheck(field);
|
|
5724
|
+
else if (!mode && state.submitted && state.errors.has(field.name)) debouncedCheck(field);
|
|
5725
|
+
};
|
|
5726
|
+
async function validate() {
|
|
5727
|
+
state.submitted = true;
|
|
5728
|
+
const fields = collectFormFields(form);
|
|
5729
|
+
const names = [];
|
|
5730
|
+
let firstInvalid = null;
|
|
5731
|
+
for (const field of fields) {
|
|
5732
|
+
if (resolveFieldRules(field).length === 0) continue;
|
|
5733
|
+
names.push(field.name);
|
|
5734
|
+
const ok = await checkField(field);
|
|
5735
|
+
if (!ok && !firstInvalid) firstInvalid = field;
|
|
5736
|
+
}
|
|
5737
|
+
const errorsSnapshot = errors();
|
|
5738
|
+
if (firstInvalid) {
|
|
5739
|
+
renderSummaryGuarded(errorsSnapshot);
|
|
5740
|
+
emitIcen(form, "icen:form-invalid", { errors: errorsSnapshot, first: firstInvalid.name });
|
|
5741
|
+
if (opts.scrollToError !== false) focusField(firstInvalid);
|
|
5742
|
+
return false;
|
|
5743
|
+
}
|
|
5744
|
+
renderSummaryGuarded({});
|
|
5745
|
+
emitIcen(form, "icen:form-valid", { names });
|
|
5746
|
+
return true;
|
|
5747
|
+
}
|
|
5748
|
+
function errors() {
|
|
5749
|
+
return Object.fromEntries(state.errors);
|
|
5750
|
+
}
|
|
5751
|
+
function reset() {
|
|
5752
|
+
state.errors.clear();
|
|
5753
|
+
state.touched.clear();
|
|
5754
|
+
state.submitted = false;
|
|
5755
|
+
state.seq.clear();
|
|
5756
|
+
for (const timer of state.debounceTimers.values()) clearTimeout(timer);
|
|
5757
|
+
state.debounceTimers.clear();
|
|
5758
|
+
pendingPresent.clear();
|
|
5759
|
+
pendingSummary = null;
|
|
5760
|
+
for (const field of collectFormFields(form)) clearFieldPresentation(field);
|
|
5761
|
+
form.querySelector(":scope > .icen-form-validate-summary")?.remove();
|
|
5762
|
+
}
|
|
5763
|
+
const onFormPointerDown = (e) => {
|
|
5764
|
+
const btn = e.button;
|
|
5765
|
+
if (typeof btn === "number" && btn !== 0) return;
|
|
5766
|
+
pointerDown = true;
|
|
5767
|
+
};
|
|
5768
|
+
const onPressEnd = () => {
|
|
5769
|
+
if (!pointerDown) return;
|
|
5770
|
+
pointerDown = false;
|
|
5771
|
+
if (pendingPresent.size > 0 || pendingSummary !== null) scheduleFlush();
|
|
5772
|
+
};
|
|
5773
|
+
form.addEventListener("submit", onSubmit);
|
|
5774
|
+
form.addEventListener("focusout", onFocusOut);
|
|
5775
|
+
form.addEventListener("input", onInput);
|
|
5776
|
+
form.addEventListener("pointerdown", onFormPointerDown, true);
|
|
5777
|
+
document.addEventListener("pointerup", onPressEnd, true);
|
|
5778
|
+
document.addEventListener("pointercancel", onPressEnd, true);
|
|
5779
|
+
window.addEventListener("blur", onPressEnd);
|
|
5780
|
+
let destroyed = false;
|
|
5781
|
+
const handle = {
|
|
5782
|
+
form,
|
|
5783
|
+
validate,
|
|
5784
|
+
async validateField(name) {
|
|
5785
|
+
const field = findFormField(form, name);
|
|
5786
|
+
if (!field || field.disabled) return true;
|
|
5787
|
+
return checkField(field);
|
|
5788
|
+
},
|
|
5789
|
+
reset,
|
|
5790
|
+
errors,
|
|
5791
|
+
destroy() {
|
|
5792
|
+
if (destroyed) return;
|
|
5793
|
+
destroyed = true;
|
|
5794
|
+
form.removeEventListener("submit", onSubmit);
|
|
5795
|
+
form.removeEventListener("focusout", onFocusOut);
|
|
5796
|
+
form.removeEventListener("input", onInput);
|
|
5797
|
+
form.removeEventListener("pointerdown", onFormPointerDown, true);
|
|
5798
|
+
document.removeEventListener("pointerup", onPressEnd, true);
|
|
5799
|
+
document.removeEventListener("pointercancel", onPressEnd, true);
|
|
5800
|
+
window.removeEventListener("blur", onPressEnd);
|
|
5801
|
+
reset();
|
|
5802
|
+
liveValidators.delete(form);
|
|
5803
|
+
}
|
|
5804
|
+
};
|
|
5805
|
+
liveValidators.set(form, handle);
|
|
5806
|
+
return handle;
|
|
5807
|
+
}
|
|
5808
|
+
var SHOW_FORM_SCHEMA = {
|
|
5809
|
+
type: "object",
|
|
5810
|
+
properties: {
|
|
5811
|
+
title: { type: "string", description: t("formValidate.tool.title") },
|
|
5812
|
+
fields: {
|
|
5813
|
+
type: "array",
|
|
5814
|
+
description: t("formValidate.tool.fields"),
|
|
5815
|
+
items: {
|
|
5816
|
+
type: "object",
|
|
5817
|
+
properties: {
|
|
5818
|
+
name: { type: "string", description: t("formValidate.tool.name") },
|
|
5819
|
+
label: { type: "string", description: t("formValidate.tool.label") },
|
|
5820
|
+
type: {
|
|
5821
|
+
type: "string",
|
|
5822
|
+
enum: ["text", "email", "number", "url", "textarea", "select", "date"],
|
|
5823
|
+
description: t("formValidate.tool.type")
|
|
5824
|
+
},
|
|
5825
|
+
required: { type: "boolean", description: t("formValidate.tool.required") },
|
|
5826
|
+
pattern: { type: "string", description: t("formValidate.tool.pattern") },
|
|
5827
|
+
min: { type: "number", description: t("formValidate.tool.min") },
|
|
5828
|
+
max: { type: "number", description: t("formValidate.tool.max") },
|
|
5829
|
+
options: {
|
|
5830
|
+
type: "array",
|
|
5831
|
+
description: t("formValidate.tool.options"),
|
|
5832
|
+
items: {
|
|
5833
|
+
anyOf: [
|
|
5834
|
+
{ type: "string" },
|
|
5835
|
+
{
|
|
5836
|
+
type: "object",
|
|
5837
|
+
properties: { label: { type: "string" }, value: { type: "string" } },
|
|
5838
|
+
required: ["label", "value"]
|
|
5839
|
+
}
|
|
5840
|
+
]
|
|
5841
|
+
}
|
|
5842
|
+
},
|
|
5843
|
+
help: { type: "string", description: t("formValidate.tool.help") }
|
|
5844
|
+
},
|
|
5845
|
+
required: ["name", "label"]
|
|
5846
|
+
}
|
|
5847
|
+
},
|
|
5848
|
+
submitLabel: { type: "string", description: t("formValidate.tool.submitText") }
|
|
5849
|
+
},
|
|
5850
|
+
required: ["fields"]
|
|
5851
|
+
};
|
|
5852
|
+
function normalizeShowForm(input) {
|
|
5853
|
+
const raw = input ?? {};
|
|
5854
|
+
const fields = [];
|
|
5855
|
+
if (Array.isArray(raw.fields)) {
|
|
5856
|
+
for (const f of raw.fields) {
|
|
5857
|
+
if (!f || typeof f !== "object") continue;
|
|
5858
|
+
const name = typeof f.name === "string" ? f.name.trim() : "";
|
|
5859
|
+
if (!name) continue;
|
|
5860
|
+
const label = typeof f.label === "string" && f.label ? f.label : name;
|
|
5861
|
+
fields.push({
|
|
5862
|
+
name,
|
|
5863
|
+
label,
|
|
5864
|
+
type: f.type,
|
|
5865
|
+
required: f.required === true,
|
|
5866
|
+
pattern: typeof f.pattern === "string" ? f.pattern : void 0,
|
|
5867
|
+
min: typeof f.min === "number" ? f.min : void 0,
|
|
5868
|
+
max: typeof f.max === "number" ? f.max : void 0,
|
|
5869
|
+
options: Array.isArray(f.options) ? f.options : void 0,
|
|
5870
|
+
help: typeof f.help === "string" ? f.help : void 0
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
return {
|
|
5875
|
+
title: typeof raw.title === "string" && raw.title ? raw.title : void 0,
|
|
5876
|
+
fields,
|
|
5877
|
+
submitLabel: typeof raw.submitLabel === "string" && raw.submitLabel ? raw.submitLabel : void 0
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
function renderShowFormField(spec) {
|
|
5881
|
+
const wrap = document.createElement("div");
|
|
5882
|
+
wrap.className = "icen-field";
|
|
5883
|
+
const id = `icen-ai-form-${++uidSeq}`;
|
|
5884
|
+
const label = document.createElement("label");
|
|
5885
|
+
label.className = "icen-field-label";
|
|
5886
|
+
label.htmlFor = id;
|
|
5887
|
+
label.textContent = spec.label;
|
|
5888
|
+
if (spec.required) {
|
|
5889
|
+
const req = document.createElement("span");
|
|
5890
|
+
req.className = "icen-req";
|
|
5891
|
+
req.textContent = "*";
|
|
5892
|
+
label.appendChild(req);
|
|
5893
|
+
}
|
|
5894
|
+
wrap.appendChild(label);
|
|
5895
|
+
let control;
|
|
5896
|
+
if (spec.type === "textarea") {
|
|
5897
|
+
const ta = document.createElement("textarea");
|
|
5898
|
+
ta.className = "icen-form-textarea";
|
|
5899
|
+
ta.setAttribute("rows", "3");
|
|
5900
|
+
control = ta;
|
|
5901
|
+
} else if (spec.type === "select") {
|
|
5902
|
+
const sel = document.createElement("select");
|
|
5903
|
+
sel.className = "icen-form-input";
|
|
5904
|
+
const placeholder = document.createElement("option");
|
|
5905
|
+
placeholder.value = "";
|
|
5906
|
+
placeholder.textContent = t("formValidate.selectPlaceholder");
|
|
5907
|
+
sel.appendChild(placeholder);
|
|
5908
|
+
for (const opt of spec.options ?? []) {
|
|
5909
|
+
const option = document.createElement("option");
|
|
5910
|
+
if (typeof opt === "string") {
|
|
5911
|
+
option.value = opt;
|
|
5912
|
+
option.textContent = opt;
|
|
5913
|
+
} else {
|
|
5914
|
+
option.value = opt.value;
|
|
5915
|
+
option.textContent = opt.label;
|
|
5916
|
+
}
|
|
5917
|
+
sel.appendChild(option);
|
|
5918
|
+
}
|
|
5919
|
+
control = sel;
|
|
5920
|
+
} else {
|
|
5921
|
+
const input = document.createElement("input");
|
|
5922
|
+
input.className = "icen-form-input";
|
|
5923
|
+
input.type = spec.type ?? "text";
|
|
5924
|
+
control = input;
|
|
5925
|
+
}
|
|
5926
|
+
control.id = id;
|
|
5927
|
+
control.name = spec.name;
|
|
5928
|
+
if (spec.required) control.required = true;
|
|
5929
|
+
if (spec.pattern) control.setAttribute("pattern", spec.pattern);
|
|
5930
|
+
if (spec.min != null) control.setAttribute("min", String(spec.min));
|
|
5931
|
+
if (spec.max != null) control.setAttribute("max", String(spec.max));
|
|
5932
|
+
wrap.appendChild(control);
|
|
5933
|
+
if (spec.help) {
|
|
5934
|
+
const hint = document.createElement("span");
|
|
5935
|
+
hint.className = "icen-field-hint";
|
|
5936
|
+
hint.textContent = spec.help;
|
|
5937
|
+
wrap.appendChild(hint);
|
|
5938
|
+
}
|
|
5939
|
+
return wrap;
|
|
5940
|
+
}
|
|
5941
|
+
function collectFormValues(form) {
|
|
5942
|
+
const out = {};
|
|
5943
|
+
for (const field of collectFormFields(form)) {
|
|
5944
|
+
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
5945
|
+
out[field.name] = field.checked;
|
|
5946
|
+
continue;
|
|
5947
|
+
}
|
|
5948
|
+
if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
5949
|
+
if (!(field.name in out)) out[field.name] = field.checked ? field.value : "";
|
|
5950
|
+
else if (field.checked) out[field.name] = field.value;
|
|
5951
|
+
continue;
|
|
5952
|
+
}
|
|
5953
|
+
out[field.name] = field.value;
|
|
5954
|
+
}
|
|
5955
|
+
return out;
|
|
5956
|
+
}
|
|
5957
|
+
function runShowForm(input, mount) {
|
|
5958
|
+
return new Promise((resolve) => {
|
|
5959
|
+
if (typeof document === "undefined") {
|
|
5960
|
+
resolve({ cancelled: true });
|
|
5961
|
+
return;
|
|
5962
|
+
}
|
|
5963
|
+
const spec = normalizeShowForm(input);
|
|
5964
|
+
const root = document.createElement("div");
|
|
5965
|
+
root.className = "icen-ai-form";
|
|
5966
|
+
if (spec.title) {
|
|
5967
|
+
const title = document.createElement("p");
|
|
5968
|
+
title.className = "icen-ai-form-title";
|
|
5969
|
+
title.textContent = spec.title;
|
|
5970
|
+
root.appendChild(title);
|
|
5971
|
+
}
|
|
5972
|
+
const form = document.createElement("form");
|
|
5973
|
+
form.className = "icen-form icen-ai-form-fields";
|
|
5974
|
+
form.setAttribute("data-icen-validate", "");
|
|
5975
|
+
form.setAttribute("data-icen-validate-summary", "");
|
|
5976
|
+
form.setAttribute("novalidate", "");
|
|
5977
|
+
for (const field of spec.fields) form.appendChild(renderShowFormField(field));
|
|
5978
|
+
const actions = document.createElement("div");
|
|
5979
|
+
actions.className = "icen-ai-form-actions";
|
|
5980
|
+
const submit = document.createElement("button");
|
|
5981
|
+
submit.type = "submit";
|
|
5982
|
+
submit.className = "icen-btn icen-btn-primary";
|
|
5983
|
+
submit.textContent = spec.submitLabel ?? t("common.submit");
|
|
5984
|
+
const cancel = document.createElement("button");
|
|
5985
|
+
cancel.type = "button";
|
|
5986
|
+
cancel.className = "icen-btn icen-btn-ghost";
|
|
5987
|
+
cancel.setAttribute("data-icen-ai-form-cancel", "");
|
|
5988
|
+
cancel.textContent = t("common.cancel");
|
|
5989
|
+
actions.append(submit, cancel);
|
|
5990
|
+
form.appendChild(actions);
|
|
5991
|
+
root.appendChild(form);
|
|
5992
|
+
mount.appendChild(root);
|
|
5993
|
+
const handle = createFormValidator(form, { scrollToError: true });
|
|
5994
|
+
const finish = (value) => {
|
|
5995
|
+
form.removeEventListener("icen:form-valid", onValid);
|
|
5996
|
+
cancel.removeEventListener("click", onCancel);
|
|
5997
|
+
handle.destroy();
|
|
5998
|
+
root.remove();
|
|
5999
|
+
resolve(value);
|
|
6000
|
+
};
|
|
6001
|
+
const onValid = () => {
|
|
6002
|
+
finish(collectFormValues(form));
|
|
6003
|
+
};
|
|
6004
|
+
const onCancel = () => {
|
|
6005
|
+
finish({ cancelled: true });
|
|
6006
|
+
};
|
|
6007
|
+
form.addEventListener("icen:form-valid", onValid);
|
|
6008
|
+
cancel.addEventListener("click", onCancel);
|
|
6009
|
+
});
|
|
6010
|
+
}
|
|
6011
|
+
|
|
5110
6012
|
// src/behaviors/ai-tools.ts
|
|
5111
6013
|
var toolRegistry = /* @__PURE__ */ new Map();
|
|
5112
6014
|
var LIVE_AREAS = /* @__PURE__ */ new Set();
|
|
@@ -5435,6 +6337,13 @@ registerAiTool({
|
|
|
5435
6337
|
return { title, rows: rows.length };
|
|
5436
6338
|
}
|
|
5437
6339
|
});
|
|
6340
|
+
registerAiTool({
|
|
6341
|
+
name: "show_form",
|
|
6342
|
+
description: t("formValidate.tool.description"),
|
|
6343
|
+
inputSchema: SHOW_FORM_SCHEMA,
|
|
6344
|
+
present: "mount",
|
|
6345
|
+
run: (input, mount) => runShowForm(input, mount)
|
|
6346
|
+
});
|
|
5438
6347
|
function isBrowserTools() {
|
|
5439
6348
|
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
5440
6349
|
}
|