@icen.ai/ui 0.9.0 → 0.10.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 +138 -80
- package/README.md +66 -91
- package/dist/base.css +208 -179
- package/dist/behaviors/accordion.mjs +8 -8
- package/dist/behaviors/ai-branch.d.mts +1 -1
- package/dist/behaviors/ai-branch.mjs +1010 -94
- package/dist/behaviors/ai-chat.d.mts +7 -7
- package/dist/behaviors/ai-chat.mjs +1071 -161
- package/dist/behaviors/ai-composer.d.mts +44 -12
- package/dist/behaviors/ai-composer.mjs +1997 -524
- package/dist/behaviors/ai-core.d.mts +1 -1
- package/dist/behaviors/ai-core.mjs +1016 -107
- package/dist/behaviors/ai-diff.mjs +1019 -127
- package/dist/behaviors/ai-feedback.d.mts +1 -1
- package/dist/behaviors/ai-feedback.mjs +1035 -126
- package/dist/behaviors/ai-panel.d.mts +8 -8
- package/dist/behaviors/ai-panel.mjs +1284 -266
- package/dist/behaviors/ai-provider.mjs +1003 -101
- package/dist/behaviors/ai-threads.d.mts +1 -1
- package/dist/behaviors/ai-threads.mjs +1068 -154
- package/dist/behaviors/ai-tool.d.mts +2 -2
- package/dist/behaviors/ai-tool.mjs +2037 -396
- package/dist/behaviors/ai-tools.d.mts +89 -16
- package/dist/behaviors/ai-tools.mjs +4448 -372
- package/dist/behaviors/autocomplete.d.mts +53 -0
- package/dist/behaviors/autocomplete.mjs +1600 -0
- package/dist/behaviors/back-top.d.mts +1 -1
- package/dist/behaviors/back-top.mjs +951 -14
- package/dist/behaviors/carousel.d.mts +1 -1
- package/dist/behaviors/carousel.mjs +906 -11
- package/dist/behaviors/cascader.d.mts +48 -0
- package/dist/behaviors/cascader.mjs +1853 -0
- package/dist/behaviors/charts.d.mts +72 -8
- package/dist/behaviors/charts.mjs +1901 -256
- package/dist/behaviors/code-block.d.mts +36 -0
- package/dist/behaviors/code-block.mjs +1611 -0
- package/dist/behaviors/command-palette.d.mts +9 -6
- package/dist/behaviors/command-palette.mjs +161 -59
- package/dist/behaviors/context-menu.d.mts +9 -8
- package/dist/behaviors/context-menu.mjs +1112 -44
- package/dist/behaviors/controls.d.mts +13 -13
- package/dist/behaviors/controls.mjs +989 -130
- package/dist/behaviors/copy.d.mts +21 -2
- package/dist/behaviors/copy.mjs +1206 -42
- package/dist/behaviors/datatable.d.mts +53 -3
- package/dist/behaviors/datatable.mjs +1963 -207
- package/dist/behaviors/date-picker.d.mts +0 -1
- package/dist/behaviors/date-picker.mjs +1340 -293
- package/dist/behaviors/design.d.mts +54 -0
- package/dist/behaviors/design.mjs +109 -0
- package/dist/behaviors/dropdown.d.mts +5 -6
- package/dist/behaviors/dropdown.mjs +262 -110
- package/dist/behaviors/events.d.mts +120 -3
- package/dist/behaviors/events.mjs +47 -2
- package/dist/behaviors/feedback.d.mts +30 -0
- package/dist/behaviors/feedback.mjs +296 -0
- package/dist/behaviors/focus.d.mts +8 -0
- package/dist/behaviors/focus.mjs +24 -0
- package/dist/behaviors/form-validate.d.mts +102 -0
- package/dist/behaviors/form-validate.mjs +6091 -0
- package/dist/behaviors/i18n.d.mts +789 -0
- package/dist/behaviors/i18n.mjs +1711 -0
- package/dist/behaviors/icons.d.mts +93 -0
- package/dist/behaviors/icons.mjs +113 -0
- package/dist/behaviors/input.d.mts +9 -1
- package/dist/behaviors/input.mjs +1056 -60
- package/dist/behaviors/kb-access.d.mts +1 -1
- package/dist/behaviors/kb-access.mjs +2129 -239
- package/dist/behaviors/kb-acl.d.mts +6 -6
- package/dist/behaviors/kb-acl.mjs +1119 -179
- package/dist/behaviors/kb-answer.mjs +2070 -418
- package/dist/behaviors/kb-audit.d.mts +1 -1
- package/dist/behaviors/kb-audit.mjs +1076 -154
- package/dist/behaviors/kb-canvas.mjs +1100 -177
- package/dist/behaviors/kb-chain.mjs +1022 -100
- package/dist/behaviors/kb-checkpoint.d.mts +1 -1
- package/dist/behaviors/kb-checkpoint.mjs +1319 -169
- package/dist/behaviors/kb-chunks.mjs +1152 -213
- package/dist/behaviors/kb-citation.d.mts +1 -1
- package/dist/behaviors/kb-citation.mjs +1201 -179
- package/dist/behaviors/kb-clarify.mjs +1058 -130
- package/dist/behaviors/kb-conflict.mjs +1041 -119
- package/dist/behaviors/kb-connector.mjs +1104 -176
- package/dist/behaviors/kb-core.d.mts +6 -1
- package/dist/behaviors/kb-core.mjs +1077 -162
- package/dist/behaviors/kb-eval.mjs +1971 -144
- package/dist/behaviors/kb-explain.d.mts +1 -1
- package/dist/behaviors/kb-explain.mjs +1024 -102
- package/dist/behaviors/kb-filter.mjs +2008 -178
- package/dist/behaviors/kb-gap.d.mts +1 -1
- package/dist/behaviors/kb-gap.mjs +1043 -121
- package/dist/behaviors/kb-hittest.d.mts +1 -1
- package/dist/behaviors/kb-hittest.mjs +1093 -150
- package/dist/behaviors/kb-metadata.mjs +1052 -130
- package/dist/behaviors/kb-passage.d.mts +2 -2
- package/dist/behaviors/kb-passage.mjs +1101 -161
- package/dist/behaviors/kb-pipeline.d.mts +2 -2
- package/dist/behaviors/kb-pipeline.mjs +1082 -162
- package/dist/behaviors/kb-qa.mjs +1088 -160
- package/dist/behaviors/kb-rerank.mjs +1047 -114
- package/dist/behaviors/kb-retrieval.mjs +2063 -224
- package/dist/behaviors/kb-review.mjs +1990 -159
- package/dist/behaviors/kb-sandbox.d.mts +1 -1
- package/dist/behaviors/kb-sandbox.mjs +1026 -104
- package/dist/behaviors/kb-segment.mjs +1111 -179
- package/dist/behaviors/kb-select.d.mts +24 -0
- package/dist/behaviors/kb-select.mjs +1989 -0
- package/dist/behaviors/kb-sources.d.mts +1 -1
- package/dist/behaviors/kb-sources.mjs +1084 -170
- package/dist/behaviors/kb-sql.d.mts +1 -1
- package/dist/behaviors/kb-sql.mjs +1143 -226
- package/dist/behaviors/kb-trace.d.mts +1 -1
- package/dist/behaviors/kb-trace.mjs +1042 -128
- package/dist/behaviors/kb-visibility.mjs +1086 -158
- package/dist/behaviors/kb-who-can.mjs +1974 -137
- package/dist/behaviors/mentions.d.mts +93 -0
- package/dist/behaviors/mentions.mjs +2152 -0
- package/dist/behaviors/modal.d.mts +4 -3
- package/dist/behaviors/modal.mjs +1055 -33
- package/dist/behaviors/nav.d.mts +1 -1
- package/dist/behaviors/nav.mjs +6 -6
- package/dist/behaviors/notification.d.mts +16 -8
- package/dist/behaviors/notification.mjs +1615 -265
- package/dist/behaviors/panel.d.mts +4 -0
- package/dist/behaviors/panel.mjs +994 -0
- package/dist/behaviors/popover.d.mts +28 -5
- package/dist/behaviors/popover.mjs +225 -24
- package/dist/behaviors/select.d.mts +5 -2
- package/dist/behaviors/select.mjs +1410 -117
- package/dist/behaviors/sidebar.d.mts +1 -1
- package/dist/behaviors/sidebar.mjs +13 -13
- package/dist/behaviors/slider.d.mts +1 -1
- package/dist/behaviors/slider.mjs +9 -9
- package/dist/behaviors/split-pane.d.mts +1 -1
- package/dist/behaviors/split-pane.mjs +7 -7
- package/dist/behaviors/suggestions.d.mts +27 -0
- package/dist/behaviors/suggestions.mjs +1089 -0
- package/dist/behaviors/svg-parse.d.mts +14 -0
- package/dist/behaviors/svg-parse.mjs +37 -0
- package/dist/behaviors/tabs.d.mts +2 -2
- package/dist/behaviors/tabs.mjs +25 -25
- package/dist/behaviors/tag-input.d.mts +23 -5
- package/dist/behaviors/tag-input.mjs +1359 -104
- package/dist/behaviors/theme-switcher.d.mts +24 -0
- package/dist/behaviors/theme-switcher.mjs +1981 -0
- package/dist/behaviors/theme.d.mts +15 -8
- package/dist/behaviors/theme.mjs +390 -28
- package/dist/behaviors/toast.d.mts +24 -25
- package/dist/behaviors/toast.mjs +1266 -88
- package/dist/behaviors/tooltip.d.mts +4 -0
- package/dist/behaviors/tooltip.mjs +378 -0
- package/dist/behaviors/tree-select.d.mts +69 -0
- package/dist/behaviors/tree-select.mjs +1907 -0
- package/dist/behaviors/tree.d.mts +1 -1
- package/dist/behaviors/tree.mjs +15 -15
- package/dist/behaviors/upload.d.mts +3 -2
- package/dist/behaviors/upload.mjs +1076 -66
- package/dist/cli.mjs +5 -6
- package/dist/components/ai-chat.css +666 -573
- package/dist/components/ai-diff.css +153 -132
- package/dist/components/ai-panel.css +351 -343
- package/dist/components/ai-tool.css +393 -238
- package/dist/components/autocomplete.css +61 -0
- package/dist/components/autocomplete.mjs +3 -0
- package/dist/components/back-top.css +34 -31
- package/dist/components/badge.css +44 -41
- package/dist/components/breadcrumb.css +67 -46
- package/dist/components/btn.css +80 -74
- package/dist/components/card.css +117 -106
- package/dist/components/cascader.css +311 -0
- package/dist/components/cascader.mjs +3 -0
- package/dist/components/chart-area.css +8 -2
- package/dist/components/chart-bar.css +95 -33
- package/dist/components/chart-gauge.css +14 -11
- package/dist/components/chart-graph.css +20 -17
- package/dist/components/chart-heatmap.css +96 -90
- package/dist/components/chart-line.css +8 -3
- package/dist/components/chart-pie.css +9 -6
- package/dist/components/chart-radar.css +8 -5
- package/dist/components/chart-scatter.css +9 -6
- package/dist/components/chart-sparkline.css +5 -2
- package/dist/components/chart-stack.css +14 -11
- package/dist/components/charts.css +164 -73
- package/dist/components/code-block.css +418 -0
- package/dist/components/code-block.mjs +4 -0
- package/dist/components/command-palette.css +138 -117
- package/dist/components/content.css +257 -251
- package/dist/components/copy.css +72 -44
- package/dist/components/datatable.css +423 -181
- package/dist/components/date-picker.css +135 -104
- package/dist/components/empty.css +68 -65
- package/dist/components/feedback.css +201 -183
- package/dist/components/form-validate.css +120 -0
- package/dist/components/form-validate.mjs +3 -0
- package/dist/components/form.css +150 -127
- package/dist/components/icon.css +102 -0
- package/dist/components/icon.mjs +3 -0
- package/dist/components/input.css +159 -104
- package/dist/components/kb-agent.css +188 -173
- package/dist/components/kb-data.css +257 -253
- package/dist/components/kb-ground.css +197 -185
- package/dist/components/kb-ingest.css +334 -350
- package/dist/components/kb-ops.css +265 -259
- package/dist/components/kb-perm.css +260 -271
- package/dist/components/kb-search.css +317 -360
- package/dist/components/kb.css +120 -109
- package/dist/components/layout.css +85 -82
- package/dist/components/material-chinese.css +73 -0
- package/dist/components/material-soviet.css +76 -0
- package/dist/components/material.css +275 -0
- package/dist/components/media.css +220 -166
- package/dist/components/mentions.css +189 -0
- package/dist/components/mentions.mjs +3 -0
- package/dist/components/menu.css +113 -105
- package/dist/components/modal.css +107 -112
- package/dist/components/nav.css +118 -115
- package/dist/components/notification.css +238 -174
- package/dist/components/pagination.css +56 -53
- package/dist/components/panel.css +66 -63
- package/dist/components/panel.mjs +1 -0
- package/dist/components/pill.css +81 -71
- package/dist/components/popover.css +67 -53
- package/dist/components/scroll-area.css +113 -110
- package/dist/components/segmented.css +146 -143
- package/dist/components/select.css +206 -186
- package/dist/components/sidebar.css +137 -127
- package/dist/components/slider.css +117 -114
- package/dist/components/split-pane.css +112 -109
- package/dist/components/stat.css +112 -106
- package/dist/components/steps.css +109 -103
- package/dist/components/switch.css +138 -134
- package/dist/components/table.css +131 -107
- package/dist/components/tabs.css +114 -108
- package/dist/components/tag-input.css +48 -133
- package/dist/components/tag.css +81 -71
- package/dist/components/theme-switcher.css +105 -0
- package/dist/components/theme-switcher.mjs +3 -0
- package/dist/components/toast.css +100 -86
- package/dist/components/toolbar.css +45 -42
- package/dist/components/tooltip.css +102 -43
- package/dist/components/tooltip.mjs +1 -0
- package/dist/components/tree-select.css +416 -0
- package/dist/components/tree-select.mjs +3 -0
- package/dist/components/upload.css +135 -92
- package/dist/fonts/LICENSES/ark-pixel/OFL.txt +96 -0
- package/dist/fonts/LICENSES/cubic-11/OFL.txt +108 -0
- package/dist/fonts/LICENSES/galmuri/LICENSE.txt +93 -0
- package/dist/fonts/OFL.txt +96 -0
- package/dist/fonts/README.md +14 -0
- package/dist/fonts/fusion-pixel-12px-zh-hans.woff2 +0 -0
- package/dist/index.d.mts +28 -14
- package/dist/index.mjs +20705 -9601
- package/dist/react.d.mts +91 -0
- package/dist/react.mjs +8208 -0
- package/dist/registry.json +130 -13
- package/dist/tokens/chinese.css +44 -0
- package/dist/tokens/colors.css +365 -1222
- package/dist/tokens/design.css +109 -0
- package/dist/tokens/dtcg.json +11198 -0
- package/dist/tokens/figma.variables.json +7017 -0
- package/dist/tokens/icon-masks.css +78 -0
- package/dist/tokens/pixel.css +50 -0
- package/dist/tokens/soviet.css +44 -0
- package/dist/tokens/style-profiles.css +36 -547
- package/dist/tokens/themes.json +308 -0
- package/dist/tokens/tokens-studio.json +6728 -0
- package/dist/tokens/typography.css +21 -18
- package/dist/tokens.css +1555 -1801
- package/dist/ui.css +11975 -9831
- package/dist/vue.d.mts +344 -0
- package/dist/vue.mjs +7781 -0
- package/docs/spec/ai-input-experience.md +75 -0
- package/docs/spec/ai-native.md +238 -147
- package/docs/spec/design-philosophy.md +189 -0
- package/docs/spec/design-primitives.md +196 -0
- package/docs/spec/kb-family.md +147 -115
- package/package.json +43 -8
- package/dist/retro-effects.css +0 -255
- package/dist/tokens/retro-effects.css +0 -255
- package/docs/research/2026-10-03-ai-kb-components.md +0 -392
- package/docs/research/2026-10-03-kb-permissions.md +0 -992
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Complete visual recipes share CSS, while each brand only supplies seeds.
|
|
2
|
+
* CSS is compiled without a DOM for SSR/static builds. Import tokens.css first.
|
|
3
|
+
* Mount a compiled recipe once, then put data-icen-preset on documentElement.
|
|
4
|
+
* Appearance belongs on html so body-portalled menus inherit the same tokens.
|
|
5
|
+
*/
|
|
6
|
+
interface DesignPalette {
|
|
7
|
+
canvas: string;
|
|
8
|
+
surface: string;
|
|
9
|
+
ink: string;
|
|
10
|
+
muted: string;
|
|
11
|
+
accent: string;
|
|
12
|
+
onAccent: string;
|
|
13
|
+
success?: string;
|
|
14
|
+
warning?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
info?: string;
|
|
17
|
+
}
|
|
18
|
+
interface DesignDefinition {
|
|
19
|
+
/** Lowercase CSS identifier; modern is the built-in 现代 recipe. */
|
|
20
|
+
name: string;
|
|
21
|
+
light: DesignPalette;
|
|
22
|
+
dark: DesignPalette;
|
|
23
|
+
/** CSS px; the shared recipe derives the six geometric roles. */
|
|
24
|
+
radius?: number;
|
|
25
|
+
space?: number;
|
|
26
|
+
controlHeight?: number;
|
|
27
|
+
fontSize?: number;
|
|
28
|
+
font?: string;
|
|
29
|
+
glassOpacity?: number;
|
|
30
|
+
glassBlur?: number;
|
|
31
|
+
motion?: number;
|
|
32
|
+
}
|
|
33
|
+
interface CompiledDesign {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly cssText: string;
|
|
36
|
+
}
|
|
37
|
+
/** WCAG sRGB contrast; six-digit opaque hex only, to keep compilation auditable. */
|
|
38
|
+
declare function designContrast(a: string, b: string): number;
|
|
39
|
+
/** Compile an independent brand with light AND dark palettes; never injects CSS.
|
|
40
|
+
* Validation covers body/muted/accent text on both surfaces and button labels.
|
|
41
|
+
* Translucent materials still need visual checks over representative content.
|
|
42
|
+
*/
|
|
43
|
+
declare function createDesign(definition: DesignDefinition): CompiledDesign;
|
|
44
|
+
/** Attach trusted, compiled author CSS; safe on the server. CSP sites can write
|
|
45
|
+
* cssText into a static stylesheet instead, or provide the page's nonce. */
|
|
46
|
+
declare function mountDesign(design: CompiledDesign, nonce?: string): () => void;
|
|
47
|
+
/** Finish palette changes without crossfading text through unreadable midtones.
|
|
48
|
+
* Call before changing design/theme attributes; hover feedback resumes next frame.
|
|
49
|
+
*/
|
|
50
|
+
declare function prepareDesignChange(): void;
|
|
51
|
+
/** Set a compiled brand theme. null restores the 现代 theme; primitives remain independent. */
|
|
52
|
+
declare function setDesign(name: string | null): void;
|
|
53
|
+
|
|
54
|
+
export { type CompiledDesign, type DesignDefinition, type DesignPalette, createDesign, designContrast, mountDesign, prepareDesignChange, setDesign };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/behaviors/design.ts
|
|
2
|
+
var NAME = /^[a-z][a-z0-9-]{1,30}$/;
|
|
3
|
+
var HEX = /^#[\da-f]{6}$/i;
|
|
4
|
+
var paletteKeys = ["canvas", "surface", "ink", "muted", "accent", "onAccent", "success", "warning", "error", "info"];
|
|
5
|
+
var seedName = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
6
|
+
function luminance(hex) {
|
|
7
|
+
const rgb = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
8
|
+
return rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;
|
|
9
|
+
}
|
|
10
|
+
function designContrast(a, b) {
|
|
11
|
+
if (!HEX.test(a) || !HEX.test(b)) throw new Error("Design colors must use #rrggbb.");
|
|
12
|
+
const x = luminance(a), y = luminance(b);
|
|
13
|
+
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
14
|
+
}
|
|
15
|
+
function compilePalette(palette, mode) {
|
|
16
|
+
const required = ["canvas", "surface", "ink", "muted", "accent", "onAccent"];
|
|
17
|
+
for (const key of required) {
|
|
18
|
+
if (typeof palette[key] !== "string" || !HEX.test(palette[key])) throw new Error(`${mode}.${key} must use #rrggbb.`);
|
|
19
|
+
}
|
|
20
|
+
for (const key of paletteKeys) {
|
|
21
|
+
const color = palette[key];
|
|
22
|
+
if (color !== void 0 && !HEX.test(color)) throw new Error(`${mode}.${key} must use #rrggbb.`);
|
|
23
|
+
}
|
|
24
|
+
for (const bg of ["canvas", "surface"]) {
|
|
25
|
+
for (const fg of ["ink", "muted", "accent"]) {
|
|
26
|
+
if (designContrast(palette[bg], palette[fg]) < 4.5) throw new Error(`${mode}.${fg} on ${bg} must reach 4.5:1 contrast.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (designContrast(palette.accent, palette.onAccent) < 4.5) throw new Error(`${mode}.onAccent must reach 4.5:1 contrast.`);
|
|
30
|
+
return paletteKeys.filter((key) => palette[key] !== void 0).map((key) => ` --icen-design-${seedName(key)}: ${palette[key]};`).join("\n") + `
|
|
31
|
+
--icen-design-accent-strong: ${palette.accent};
|
|
32
|
+
--icen-design-accent-text: ${palette.accent};
|
|
33
|
+
--icen-design-faint: ${palette.muted};
|
|
34
|
+
--icen-design-raised: color-mix(in srgb, ${palette.surface} 96%, ${palette.ink});`;
|
|
35
|
+
}
|
|
36
|
+
function createDesign(definition) {
|
|
37
|
+
if (!NAME.test(definition.name)) throw new Error("Invalid design name.");
|
|
38
|
+
const knobs = [];
|
|
39
|
+
const ranges = {
|
|
40
|
+
radius: [0, 32, "px"],
|
|
41
|
+
space: [2, 8, "px"],
|
|
42
|
+
controlHeight: [32, 56, "px"],
|
|
43
|
+
fontSize: [12, 24, "px"],
|
|
44
|
+
glassOpacity: [70, 100, "%"],
|
|
45
|
+
glassBlur: [0, 40, "px"],
|
|
46
|
+
motion: [0, 1, ""]
|
|
47
|
+
};
|
|
48
|
+
for (const key of Object.keys(ranges)) {
|
|
49
|
+
const value = definition[key];
|
|
50
|
+
if (value === void 0) continue;
|
|
51
|
+
const [min, max, unit] = ranges[key];
|
|
52
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new Error(`${key} must be between ${min} and ${max}.`);
|
|
53
|
+
knobs.push(` --icen-design-${seedName(key)}: ${value}${unit};`);
|
|
54
|
+
}
|
|
55
|
+
if (definition.font !== void 0) {
|
|
56
|
+
if (!definition.font.trim() || /[;{}<>\\\n\r]|\/\*/.test(definition.font)) throw new Error("Invalid font stack.");
|
|
57
|
+
knobs.push(` --icen-design-font: ${definition.font};`);
|
|
58
|
+
}
|
|
59
|
+
const selector = `:root[data-icen-preset="${definition.name}"]`;
|
|
60
|
+
const cssText = `${selector} {
|
|
61
|
+
${compilePalette(definition.light, "light")}
|
|
62
|
+
${knobs.join("\n")}
|
|
63
|
+
}
|
|
64
|
+
${selector}[data-icen-theme="dark"] {
|
|
65
|
+
${compilePalette(definition.dark, "dark")}
|
|
66
|
+
${knobs.join("\n")}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
return Object.freeze({ name: definition.name, cssText });
|
|
70
|
+
}
|
|
71
|
+
function mountDesign(design, nonce) {
|
|
72
|
+
if (typeof document === "undefined") return () => {
|
|
73
|
+
};
|
|
74
|
+
const style = document.createElement("style");
|
|
75
|
+
if (nonce) style.nonce = nonce;
|
|
76
|
+
style.textContent = design.cssText;
|
|
77
|
+
document.head.append(style);
|
|
78
|
+
return () => style.remove();
|
|
79
|
+
}
|
|
80
|
+
var appearanceFrame;
|
|
81
|
+
function prepareDesignChange() {
|
|
82
|
+
if (typeof document === "undefined" || typeof window.requestAnimationFrame !== "function") return;
|
|
83
|
+
const root = document.documentElement;
|
|
84
|
+
root.setAttribute("data-icen-design-changing", "");
|
|
85
|
+
void root.offsetWidth;
|
|
86
|
+
if (appearanceFrame !== void 0) window.cancelAnimationFrame(appearanceFrame);
|
|
87
|
+
appearanceFrame = window.requestAnimationFrame(() => {
|
|
88
|
+
appearanceFrame = window.requestAnimationFrame(() => {
|
|
89
|
+
void root.offsetWidth;
|
|
90
|
+
root.removeAttribute("data-icen-design-changing");
|
|
91
|
+
appearanceFrame = void 0;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function setDesign(name) {
|
|
96
|
+
if (name !== null && !NAME.test(name)) throw new Error("Invalid design name.");
|
|
97
|
+
if (typeof document === "undefined") return;
|
|
98
|
+
prepareDesignChange();
|
|
99
|
+
document.documentElement.setAttribute("data-icen-preset", name ?? "modern");
|
|
100
|
+
if (!document.documentElement.hasAttribute("data-icen-style")) document.documentElement.setAttribute("data-icen-style", "modern");
|
|
101
|
+
document.documentElement.removeAttribute("data-icen-design");
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
createDesign,
|
|
105
|
+
designContrast,
|
|
106
|
+
mountDesign,
|
|
107
|
+
prepareDesignChange,
|
|
108
|
+
setDesign
|
|
109
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
1
|
+
/** Open an authored menu and focus its first/last available action. */
|
|
2
|
+
declare function openDropdown(trigger: HTMLElement, edge?: 'first' | 'last'): void;
|
|
3
|
+
/** Closing is interruptible: reopening/destroying first restores template nodes. */
|
|
4
|
+
declare function closeDropdown(restoreFocus?: boolean): void;
|
|
6
5
|
declare function initDropdown(root?: ParentNode): () => void;
|
|
7
6
|
|
|
8
|
-
export { initDropdown };
|
|
7
|
+
export { closeDropdown, initDropdown, openDropdown };
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
// src/behaviors/popover.ts
|
|
2
|
+
function revealPopoverItem(item, scrollport, opts = {}) {
|
|
3
|
+
if (typeof document === "undefined") return;
|
|
4
|
+
if (opts.focus) item.focus({ preventScroll: true });
|
|
5
|
+
const rect = item.getBoundingClientRect(), bounds = scrollport.getBoundingClientRect();
|
|
6
|
+
const style = getComputedStyle(scrollport);
|
|
7
|
+
const inset = (value, extent) => {
|
|
8
|
+
const amount = Number.parseFloat(value) || 0;
|
|
9
|
+
return value.endsWith("%") ? amount * extent / 100 : amount;
|
|
10
|
+
};
|
|
11
|
+
if (opts.axis !== "inline") {
|
|
12
|
+
const top = bounds.top + scrollport.clientTop + inset(style.scrollPaddingTop, scrollport.clientHeight);
|
|
13
|
+
const bottom = bounds.top + scrollport.clientTop + scrollport.clientHeight - inset(style.scrollPaddingBottom, scrollport.clientHeight);
|
|
14
|
+
if (rect.top < top) scrollport.scrollTop -= top - rect.top;
|
|
15
|
+
else if (rect.bottom > bottom) scrollport.scrollTop += rect.bottom - bottom;
|
|
16
|
+
}
|
|
17
|
+
if (opts.axis === "inline" || opts.axis === "both") {
|
|
18
|
+
const left = bounds.left + scrollport.clientLeft + inset(style.scrollPaddingLeft, scrollport.clientWidth);
|
|
19
|
+
const right = bounds.left + scrollport.clientLeft + scrollport.clientWidth - inset(style.scrollPaddingRight, scrollport.clientWidth);
|
|
20
|
+
if (rect.left < left) scrollport.scrollLeft -= left - rect.left;
|
|
21
|
+
else if (rect.right > right) scrollport.scrollLeft += rect.right - right;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
2
24
|
var PANEL_SIZING_ATTRS = {
|
|
3
|
-
width: "data-panel-width",
|
|
4
|
-
minWidth: "data-panel-min",
|
|
5
|
-
maxWidth: "data-panel-max",
|
|
6
|
-
minHeight: "data-panel-min-height",
|
|
7
|
-
maxHeight: "data-panel-max-height"
|
|
25
|
+
width: "data-icen-panel-width",
|
|
26
|
+
minWidth: "data-icen-panel-min",
|
|
27
|
+
maxWidth: "data-icen-panel-max",
|
|
28
|
+
minHeight: "data-icen-panel-min-height",
|
|
29
|
+
maxHeight: "data-icen-panel-max-height"
|
|
8
30
|
};
|
|
9
31
|
function readPanelSizing(el) {
|
|
10
32
|
const out = {};
|
|
@@ -65,10 +87,10 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
|
|
|
65
87
|
const preferredSide = opts.side ?? "top";
|
|
66
88
|
const align = opts.align ?? "start";
|
|
67
89
|
if (preferredSide === "left" || preferredSide === "right") {
|
|
90
|
+
const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
|
|
68
91
|
const availableLeft = Math.max(0, anchor.left - margin - offset);
|
|
69
92
|
const availableRight = Math.max(0, viewport.width - anchor.right - margin - offset);
|
|
70
|
-
const side2 = (preferredSide === "left" ? availableLeft : availableRight) >=
|
|
71
|
-
const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
|
|
93
|
+
const side2 = (preferredSide === "left" ? availableLeft : availableRight) >= minWidth2 ? preferredSide : preferredSide === "left" ? "right" : "left";
|
|
72
94
|
const maxWidth2 = Math.min(
|
|
73
95
|
Math.max(opts.maxWidth ?? DEFAULT_MAX_WIDTH, minWidth2),
|
|
74
96
|
side2 === "left" ? availableLeft : availableRight
|
|
@@ -97,7 +119,7 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
|
|
|
97
119
|
const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
|
|
98
120
|
const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
|
|
99
121
|
const fallbackHeight = Math.min(260, requestedMaxHeight);
|
|
100
|
-
const contentHeight =
|
|
122
|
+
const contentHeight = opts.contentHeightHint != null && opts.contentHeightHint > 0 ? opts.contentHeightHint : fallbackHeight;
|
|
101
123
|
const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
|
|
102
124
|
const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
|
|
103
125
|
const sideSpace = side === "top" ? availableTop : availableBottom;
|
|
@@ -113,6 +135,14 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
|
|
|
113
135
|
const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
|
|
114
136
|
return { side, left, top, width, maxHeight };
|
|
115
137
|
}
|
|
138
|
+
var menuCloser = null;
|
|
139
|
+
function claimMenuFocus(close) {
|
|
140
|
+
menuCloser?.();
|
|
141
|
+
menuCloser = close;
|
|
142
|
+
return () => {
|
|
143
|
+
if (menuCloser === close) menuCloser = null;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
116
146
|
|
|
117
147
|
// src/behaviors/events.ts
|
|
118
148
|
function emitIcen(source, type, detail) {
|
|
@@ -126,154 +156,276 @@ function emitMenuSelect(source, item) {
|
|
|
126
156
|
emitIcen(item, "icen:menu-select", {
|
|
127
157
|
source,
|
|
128
158
|
item,
|
|
129
|
-
value: item.dataset.
|
|
159
|
+
value: item.dataset.icenValue ?? "",
|
|
130
160
|
/* 优先专用 label 子元素(避开快捷键/图标文本混入),无则退整项文本 */
|
|
131
|
-
label: (item.querySelector(".menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
|
|
161
|
+
label: (item.querySelector(".icen-menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
|
|
132
162
|
});
|
|
133
163
|
}
|
|
134
164
|
|
|
135
165
|
// src/behaviors/dropdown.ts
|
|
136
|
-
var VIEWPORT_MARGIN = 12;
|
|
137
|
-
var SIDE_OFFSET = 4;
|
|
138
166
|
var active = null;
|
|
139
|
-
var
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
var sequence = 0;
|
|
168
|
+
var scopes = /* @__PURE__ */ new Map();
|
|
169
|
+
var finishClosing = null;
|
|
170
|
+
var releaseMenu = null;
|
|
171
|
+
var classDisabled = /* @__PURE__ */ new WeakSet();
|
|
172
|
+
var unavailable = (item) => item.matches(".is-disabled,:disabled") || Boolean(item.closest('[aria-disabled="true"],[hidden],[inert],[data-icen-disabled]'));
|
|
173
|
+
var items = (menu) => Array.from(menu.querySelectorAll(".icen-menu-item")).filter((item) => !unavailable(item));
|
|
174
|
+
var within = (scope, node) => scope === node || scope instanceof Node && scope.contains(node);
|
|
175
|
+
function setHighlight(index, focus = true) {
|
|
144
176
|
if (!active) return;
|
|
145
|
-
|
|
146
|
-
active.highlight =
|
|
177
|
+
const rows = items(active.panel);
|
|
178
|
+
active.highlight = index;
|
|
179
|
+
active.panel.querySelectorAll(".icen-menu-item").forEach((item) => {
|
|
180
|
+
const selected = item === rows[index];
|
|
181
|
+
item.classList.toggle("is-highlighted", selected);
|
|
182
|
+
item.tabIndex = selected ? 0 : -1;
|
|
183
|
+
});
|
|
184
|
+
if (rows[index]) revealPopoverItem(rows[index], active.panel, { focus });
|
|
147
185
|
}
|
|
148
186
|
function place() {
|
|
149
187
|
if (!active) return;
|
|
150
188
|
const { trigger, panel } = active;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
189
|
+
if (!trigger.isConnected) {
|
|
190
|
+
closeDropdown(false);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const sizing = readPanelSizing(trigger.closest("[data-icen-dropdown]"));
|
|
194
|
+
applyPanelSizing(panel, null);
|
|
195
|
+
applyPanelSizing(panel, sizing);
|
|
196
|
+
const width = panel.offsetWidth || panel.getBoundingClientRect().width || 180;
|
|
197
|
+
const height = panel.scrollHeight || panel.offsetHeight;
|
|
198
|
+
const cssMax = parseFloat(getComputedStyle(panel).maxHeight);
|
|
199
|
+
const layout = computePopoverLayout(trigger.getBoundingClientRect(), { width: window.innerWidth, height: window.innerHeight }, {
|
|
200
|
+
side: "bottom",
|
|
201
|
+
align: getComputedStyle(trigger).direction === "rtl" ? "end" : "start",
|
|
202
|
+
offset: 4,
|
|
203
|
+
margin: 12,
|
|
204
|
+
minWidth: width,
|
|
205
|
+
maxWidth: width,
|
|
206
|
+
minHeight: 40,
|
|
207
|
+
maxHeight: sizing.maxHeight ?? (Number.isFinite(cssMax) ? cssMax : 420),
|
|
208
|
+
contentHeightHint: height
|
|
209
|
+
});
|
|
157
210
|
panel.style.left = `${layout.left}px`;
|
|
158
211
|
panel.style.top = `${layout.top}px`;
|
|
159
|
-
panel.
|
|
212
|
+
panel.style.width = `${layout.width}px`;
|
|
213
|
+
panel.style.minWidth = `${layout.width}px`;
|
|
214
|
+
panel.style.maxHeight = `${layout.maxHeight}px`;
|
|
215
|
+
panel.dataset.icenSide = layout.side;
|
|
160
216
|
}
|
|
161
|
-
function openDropdown(trigger,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
function openDropdown(trigger, edge = "first") {
|
|
218
|
+
if (typeof document === "undefined" || unavailable(trigger)) return;
|
|
219
|
+
closeDropdown(false);
|
|
220
|
+
finishClosing?.();
|
|
221
|
+
const root = trigger.closest("[data-icen-dropdown]");
|
|
222
|
+
const template = root?.querySelector("[data-icen-dropdown-menu]");
|
|
165
223
|
if (!root || !template) return;
|
|
224
|
+
releaseMenu = claimMenuFocus(() => closeDropdown(false));
|
|
225
|
+
if (trigger instanceof HTMLButtonElement && !trigger.hasAttribute("type")) trigger.type = "button";
|
|
226
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
166
227
|
const panel = document.createElement("div");
|
|
167
|
-
panel.className = "menu";
|
|
228
|
+
panel.className = "icen-menu";
|
|
229
|
+
panel.id = `icen-dropdown-${++sequence}`;
|
|
168
230
|
panel.setAttribute("role", "menu");
|
|
231
|
+
panel.tabIndex = -1;
|
|
169
232
|
panel.style.visibility = "hidden";
|
|
233
|
+
trigger.id ||= `icen-dropdown-trigger-${sequence}`;
|
|
234
|
+
panel.setAttribute("aria-labelledby", trigger.id);
|
|
235
|
+
trigger.setAttribute("aria-controls", panel.id);
|
|
170
236
|
applyPanelSizing(panel, readPanelSizing(root));
|
|
171
|
-
while (template.firstChild) panel.
|
|
172
|
-
panel.querySelectorAll(".menu-item").forEach((
|
|
173
|
-
|
|
174
|
-
|
|
237
|
+
while (template.firstChild) panel.append(template.firstChild);
|
|
238
|
+
panel.querySelectorAll(".icen-menu-item").forEach((item) => {
|
|
239
|
+
if (item instanceof HTMLButtonElement && !item.hasAttribute("type")) item.type = "button";
|
|
240
|
+
if (!item.hasAttribute("role")) item.setAttribute("role", item.hasAttribute("aria-checked") || item.classList.contains("icen-menu-item--checked") ? "menuitemcheckbox" : "menuitem");
|
|
241
|
+
if (item.getAttribute("role") === "menuitemcheckbox" && !item.hasAttribute("aria-checked")) item.setAttribute("aria-checked", String(item.classList.contains("icen-menu-item--checked")));
|
|
242
|
+
if (classDisabled.has(item)) {
|
|
243
|
+
item.removeAttribute("aria-disabled");
|
|
244
|
+
classDisabled.delete(item);
|
|
245
|
+
}
|
|
246
|
+
if (item.classList.contains("is-disabled") && !item.hasAttribute("aria-disabled")) {
|
|
247
|
+
item.setAttribute("aria-disabled", "true");
|
|
248
|
+
classDisabled.add(item);
|
|
249
|
+
}
|
|
250
|
+
item.tabIndex = -1;
|
|
251
|
+
item.querySelector(".icen-menu-item-check")?.remove();
|
|
252
|
+
item.querySelector(".icen-menu-item-shortcut")?.setAttribute("aria-hidden", "true");
|
|
253
|
+
});
|
|
254
|
+
panel.querySelectorAll(".icen-menu-separator").forEach((separator) => separator.setAttribute("role", "separator"));
|
|
255
|
+
document.body.append(panel);
|
|
256
|
+
active = { trigger, template, panel, highlight: -1, typed: "", typedAt: 0 };
|
|
175
257
|
trigger.setAttribute("aria-expanded", "true");
|
|
176
258
|
place();
|
|
177
259
|
panel.style.visibility = "";
|
|
178
|
-
|
|
260
|
+
const rows = items(panel);
|
|
261
|
+
if (rows.length) setHighlight(edge === "last" ? rows.length - 1 : 0);
|
|
262
|
+
else panel.focus({ preventScroll: true });
|
|
263
|
+
panel.addEventListener("pointermove", onPointerMove);
|
|
264
|
+
panel.addEventListener("focusin", (event) => {
|
|
265
|
+
const item = event.target instanceof Element ? event.target.closest(".icen-menu-item") : null;
|
|
266
|
+
if (active?.panel === panel && item) setHighlight(items(panel).indexOf(item), false);
|
|
267
|
+
});
|
|
268
|
+
panel.addEventListener("click", (event) => {
|
|
269
|
+
const item = event.target instanceof Element ? event.target.closest(".icen-menu-item") : null;
|
|
270
|
+
if (item && unavailable(item)) {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
event.stopImmediatePropagation();
|
|
273
|
+
}
|
|
274
|
+
}, true);
|
|
275
|
+
document.addEventListener("click", onClick);
|
|
179
276
|
document.addEventListener("pointerdown", onOutsidePointerDown, true);
|
|
180
277
|
document.addEventListener("scroll", onScroll, true);
|
|
181
|
-
window.addEventListener("
|
|
278
|
+
window.addEventListener("resize", place);
|
|
279
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
182
280
|
}
|
|
183
|
-
function closeDropdown(restoreFocus =
|
|
281
|
+
function closeDropdown(restoreFocus = true) {
|
|
184
282
|
if (!active) return;
|
|
185
283
|
const { trigger, template, panel } = active;
|
|
186
284
|
active = null;
|
|
285
|
+
releaseMenu?.();
|
|
286
|
+
releaseMenu = null;
|
|
187
287
|
trigger.setAttribute("aria-expanded", "false");
|
|
188
|
-
|
|
189
|
-
panel.
|
|
288
|
+
trigger.removeAttribute("aria-controls");
|
|
289
|
+
panel.removeEventListener("pointermove", onPointerMove);
|
|
190
290
|
document.removeEventListener("pointerdown", onOutsidePointerDown, true);
|
|
191
291
|
document.removeEventListener("scroll", onScroll, true);
|
|
192
|
-
window.removeEventListener("
|
|
193
|
-
|
|
292
|
+
window.removeEventListener("resize", place);
|
|
293
|
+
window.removeEventListener("keydown", onKeyDown, true);
|
|
294
|
+
if (!scopes.size) document.removeEventListener("click", onClick);
|
|
295
|
+
if (restoreFocus && trigger.isConnected) trigger.focus({ preventScroll: true });
|
|
296
|
+
panel.inert = true;
|
|
297
|
+
panel.setAttribute("aria-hidden", "true");
|
|
298
|
+
panel.dataset.icenState = "closing";
|
|
299
|
+
let done = false;
|
|
300
|
+
let animation;
|
|
301
|
+
const finish = () => {
|
|
302
|
+
if (done) return;
|
|
303
|
+
done = true;
|
|
304
|
+
animation?.cancel();
|
|
305
|
+
while (panel.firstChild) template.append(panel.firstChild);
|
|
306
|
+
panel.remove();
|
|
307
|
+
if (finishClosing === finish) finishClosing = null;
|
|
308
|
+
};
|
|
309
|
+
finishClosing = finish;
|
|
310
|
+
const css = getComputedStyle(panel), duration = parseFloat(css.animationDuration) * (css.animationDuration.endsWith("ms") ? 1 : 1e3);
|
|
311
|
+
if (typeof panel.animate === "function" && trigger.isConnected && duration !== 0 && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
312
|
+
animation = panel.animate([{ opacity: css.opacity, transform: css.transform === "none" ? "scale(1)" : css.transform }, { opacity: 0, transform: "scale(.985)" }], { duration: Number.isFinite(duration) ? Math.min(90, duration * 0.75) : 90, easing: css.getPropertyValue("--icen-ease-out").trim() || "ease-out", fill: "forwards" });
|
|
313
|
+
void animation.finished.then(finish, finish);
|
|
314
|
+
} else finish();
|
|
194
315
|
}
|
|
195
|
-
function onOutsidePointerDown(
|
|
196
|
-
if (!active)
|
|
197
|
-
const target = e.target;
|
|
198
|
-
if (!(target instanceof Node)) return;
|
|
199
|
-
if (active.panel.contains(target) || active.trigger.contains(target)) return;
|
|
200
|
-
closeDropdown();
|
|
316
|
+
function onOutsidePointerDown(event) {
|
|
317
|
+
if (active && event.target instanceof Node && !active.panel.contains(event.target) && !active.trigger.contains(event.target)) closeDropdown(false);
|
|
201
318
|
}
|
|
202
|
-
function onScroll(
|
|
203
|
-
if (!active)
|
|
204
|
-
const target = e.target;
|
|
205
|
-
if (target instanceof Node && active.panel.contains(target)) return;
|
|
206
|
-
closeDropdown();
|
|
319
|
+
function onScroll(event) {
|
|
320
|
+
if (active && !(event.target instanceof Node && active.panel.contains(event.target))) closeDropdown(false);
|
|
207
321
|
}
|
|
208
|
-
function
|
|
209
|
-
if (!active) return;
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
322
|
+
function onPointerMove(event) {
|
|
323
|
+
if (!active || event.pointerType === "touch" || !(event.target instanceof Element)) return;
|
|
324
|
+
const item = event.target.closest(".icen-menu-item");
|
|
325
|
+
if (item && active.panel.contains(item)) setHighlight(items(active.panel).indexOf(item), false);
|
|
326
|
+
}
|
|
327
|
+
function onKeyDown(event) {
|
|
328
|
+
if (!active || event.isComposing || event.keyCode === 229) return;
|
|
329
|
+
if (event.key === "Escape") {
|
|
330
|
+
event.preventDefault();
|
|
331
|
+
event.stopPropagation();
|
|
332
|
+
closeDropdown();
|
|
214
333
|
return;
|
|
215
334
|
}
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
335
|
+
if (event.key === "Tab") {
|
|
336
|
+
closeDropdown();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (event.target instanceof Element && event.target.matches('input,textarea,[contenteditable="true"]')) return;
|
|
340
|
+
const rows = items(active.panel);
|
|
341
|
+
if (!rows.length) return;
|
|
342
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? rows.length - 1 : (active.highlight + (event.key === "ArrowDown" ? 1 : -1) + rows.length) % rows.length;
|
|
221
345
|
setHighlight(next);
|
|
222
346
|
return;
|
|
223
347
|
}
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
348
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
rows[active.highlight]?.click();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
354
|
+
event.preventDefault();
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
active.typed = now - active.typedAt < 600 ? active.typed + event.key : event.key;
|
|
357
|
+
active.typedAt = now;
|
|
358
|
+
let query = active.typed.toLocaleLowerCase();
|
|
359
|
+
if ([...query].every((character) => character === query[0])) query = active.typed = query[0];
|
|
360
|
+
for (let step = 1; step <= rows.length; step++) {
|
|
361
|
+
const index = (Math.max(0, active.highlight) + (query.length === 1 ? step : step - 1)) % rows.length;
|
|
362
|
+
const label = rows[index].querySelector(".icen-menu-item-label")?.textContent ?? rows[index].textContent ?? "";
|
|
363
|
+
if (label.trim().toLocaleLowerCase().startsWith(query)) {
|
|
364
|
+
setHighlight(index);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function onClick(event) {
|
|
370
|
+
if (!(event.target instanceof Element)) return;
|
|
371
|
+
const item = event.target.closest(".icen-menu-item");
|
|
372
|
+
if (active && item && active.panel.contains(item)) {
|
|
373
|
+
if (unavailable(item)) {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
event.stopPropagation();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const current = active;
|
|
379
|
+
emitMenuSelect(current.panel, item);
|
|
380
|
+
if (active === current && !item.hasAttribute("data-icen-menu-keep-open")) closeDropdown(current.panel.contains(document.activeElement));
|
|
381
|
+
return;
|
|
230
382
|
}
|
|
383
|
+
const trigger = event.target.closest("[data-icen-dropdown-trigger]");
|
|
384
|
+
if (!trigger || ![...scopes.keys()].some((scope) => within(scope, trigger))) return;
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
if (active?.trigger === trigger) closeDropdown();
|
|
387
|
+
else openDropdown(trigger);
|
|
388
|
+
}
|
|
389
|
+
function onTriggerKey(event) {
|
|
390
|
+
if (active || event.isComposing || !["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return;
|
|
391
|
+
const trigger = event.target instanceof Element ? event.target.closest("[data-icen-dropdown-trigger]") : null;
|
|
392
|
+
if (!trigger || ![...scopes.keys()].some((scope) => within(scope, trigger))) return;
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
openDropdown(trigger, event.key === "ArrowUp" ? "last" : "first");
|
|
395
|
+
event.stopPropagation();
|
|
231
396
|
}
|
|
232
397
|
function initDropdown(root) {
|
|
233
398
|
if (typeof document === "undefined") return () => {
|
|
234
399
|
};
|
|
235
|
-
const scope = root ?? document;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
400
|
+
const scope = root ?? document, existing = scopes.get(scope);
|
|
401
|
+
if (existing) return existing;
|
|
402
|
+
const triggers = [...scope instanceof HTMLElement && scope.matches("[data-icen-dropdown-trigger]") ? [scope] : [], ...scope.querySelectorAll("[data-icen-dropdown-trigger]")];
|
|
403
|
+
triggers.forEach((trigger) => {
|
|
404
|
+
if (active?.trigger !== trigger) trigger.setAttribute("aria-expanded", "false");
|
|
405
|
+
trigger.setAttribute("aria-haspopup", "menu");
|
|
406
|
+
if (trigger instanceof HTMLButtonElement && !trigger.hasAttribute("type")) trigger.type = "button";
|
|
407
|
+
});
|
|
408
|
+
if (!scopes.size) {
|
|
409
|
+
document.addEventListener("click", onClick);
|
|
410
|
+
document.addEventListener("keydown", onTriggerKey);
|
|
411
|
+
}
|
|
412
|
+
let disposed = false;
|
|
413
|
+
const dispose = () => {
|
|
414
|
+
if (disposed) return;
|
|
415
|
+
disposed = true;
|
|
416
|
+
scopes.delete(scope);
|
|
417
|
+
if (active && within(scope, active.trigger)) closeDropdown(false);
|
|
418
|
+
finishClosing?.();
|
|
419
|
+
if (!scopes.size) {
|
|
420
|
+
if (!active) document.removeEventListener("click", onClick);
|
|
421
|
+
document.removeEventListener("keydown", onTriggerKey);
|
|
252
422
|
}
|
|
253
|
-
const trigger = target.closest("[data-dropdown-trigger]");
|
|
254
|
-
if (!trigger) return;
|
|
255
|
-
if (active && active.trigger === trigger) closeDropdown();
|
|
256
|
-
else openDropdown(trigger);
|
|
257
|
-
};
|
|
258
|
-
document.addEventListener("click", onClick);
|
|
259
|
-
const onKeyDown2 = (e) => {
|
|
260
|
-
if (e.key !== "ArrowDown") return;
|
|
261
|
-
if (active) return;
|
|
262
|
-
const target = e.target instanceof Element ? e.target : null;
|
|
263
|
-
const trigger = target?.closest("[data-dropdown-trigger]");
|
|
264
|
-
if (!trigger) return;
|
|
265
|
-
e.preventDefault();
|
|
266
|
-
openDropdown(trigger, true);
|
|
267
|
-
};
|
|
268
|
-
document.addEventListener("keydown", onKeyDown2);
|
|
269
|
-
const destroy = () => {
|
|
270
|
-
document.removeEventListener("click", onClick);
|
|
271
|
-
document.removeEventListener("keydown", onKeyDown2);
|
|
272
|
-
closeDropdown();
|
|
273
|
-
initialized = false;
|
|
274
423
|
};
|
|
275
|
-
|
|
424
|
+
scopes.set(scope, dispose);
|
|
425
|
+
return dispose;
|
|
276
426
|
}
|
|
277
427
|
export {
|
|
278
|
-
|
|
428
|
+
closeDropdown,
|
|
429
|
+
initDropdown,
|
|
430
|
+
openDropdown
|
|
279
431
|
};
|