@wsxjs/wsx-base-components 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +28 -28
  3. package/dist/index.cjs +14 -2
  4. package/dist/index.js +4971 -2032
  5. package/dist/style.css +1 -1
  6. package/package.json +16 -7
  7. package/src/{XyButton.css → Button.css} +1 -1
  8. package/src/{XyButton.wsx → Button.wsx} +18 -9
  9. package/src/ButtonGroup.css +30 -0
  10. package/src/{XyButtonGroup.wsx → ButtonGroup.wsx} +26 -14
  11. package/src/CodeBlock.css +275 -0
  12. package/src/CodeBlock.types.ts +25 -0
  13. package/src/CodeBlock.wsx +296 -0
  14. package/src/ColorPicker.wsx +6 -5
  15. package/src/Combobox.css +254 -0
  16. package/src/Combobox.types.ts +32 -0
  17. package/src/Combobox.wsx +352 -0
  18. package/src/Dropdown.css +178 -0
  19. package/src/Dropdown.types.ts +28 -0
  20. package/src/Dropdown.wsx +221 -0
  21. package/src/LanguageSwitcher.css +148 -0
  22. package/src/LanguageSwitcher.wsx +190 -0
  23. package/src/OverflowDetector.ts +169 -0
  24. package/src/ResponsiveNav.css +555 -0
  25. package/src/ResponsiveNav.types.ts +30 -0
  26. package/src/ResponsiveNav.wsx +450 -0
  27. package/src/SvgIcon.wsx +2 -2
  28. package/src/index.ts +17 -9
  29. package/src/types/wsx.d.ts +4 -3
  30. package/src/ReactiveCounter.css +0 -304
  31. package/src/ReactiveCounter.wsx +0 -231
  32. package/src/SimpleReactiveDemo.wsx +0 -59
  33. package/src/SvgDemo.wsx +0 -241
  34. package/src/TodoList.css +0 -197
  35. package/src/TodoList.wsx +0 -264
  36. package/src/TodoListLight.css +0 -198
  37. package/src/TodoListLight.wsx +0 -263
  38. package/src/UserProfile.css +0 -146
  39. package/src/UserProfile.wsx +0 -247
  40. package/src/UserProfileLight.css +0 -146
  41. package/src/UserProfileLight.wsx +0 -256
  42. package/src/XyButtonGroup.css +0 -30
@@ -0,0 +1,221 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * Dropdown Component
4
+ * 通用下拉菜单组件,用于构建各种下拉选择器
5
+ */
6
+ import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
7
+ import styles from "./Dropdown.css?inline";
8
+ import type { DropdownOption, DropdownConfig } from "./Dropdown.types";
9
+ export type { DropdownOption, DropdownConfig };
10
+
11
+ @autoRegister({ tagName: "wsx-dropdown" })
12
+ export default class Dropdown extends WebComponent {
13
+ /** 选项列表 */
14
+ private options: DropdownOption[] = [];
15
+ /** 当前选中的值 */
16
+ @state private selectedValue = "";
17
+ /** 下拉菜单是否打开 */
18
+ @state private isOpen: boolean = false;
19
+ /** 配置选项 */
20
+ private config: DropdownConfig = {};
21
+
22
+ /** 元素引用 */
23
+ private buttonElement?: HTMLElement;
24
+ private dropdownElement?: HTMLElement;
25
+ private outsideClickHandler?: (event: Event) => void;
26
+
27
+ constructor(config: DropdownConfig = {}) {
28
+ super({
29
+ styles,
30
+ styleName: "wsx-dropdown",
31
+ });
32
+ this.config = {
33
+ placeholder: "Select...",
34
+ align: "left",
35
+ position: "bottom",
36
+ trigger: "click",
37
+ ...config,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * 设置选项列表
43
+ */
44
+ public setOptions(options: DropdownOption[]): void {
45
+ this.options = options;
46
+ this.rerender();
47
+ }
48
+
49
+ /**
50
+ * 设置选中的值
51
+ */
52
+ public setValue(value: string): void {
53
+ this.selectedValue = value;
54
+ this.rerender();
55
+ this.dispatchEvent(
56
+ new CustomEvent("change", {
57
+ detail: { value },
58
+ bubbles: true,
59
+ })
60
+ );
61
+ }
62
+
63
+ /**
64
+ * 获取选中的值
65
+ */
66
+ public getValue(): string | undefined {
67
+ return this.selectedValue || undefined;
68
+ }
69
+
70
+ render(): HTMLElement {
71
+ const selectedOption = this.options.find(
72
+ (opt) => opt.value === this.selectedValue && this.selectedValue !== ""
73
+ );
74
+ const displayText = selectedOption?.label || this.config.placeholder || "Select...";
75
+
76
+ return (
77
+ <div class="dropdown-container">
78
+ <button
79
+ ref={(el) => (this.buttonElement = el)}
80
+ class={`dropdown-button ${this.isOpen ? "open" : ""} ${
81
+ this.config.disabled ? "disabled" : ""
82
+ }`}
83
+ onClick={this.config.trigger === "click" ? this.toggleDropdown : undefined}
84
+ onMouseEnter={this.config.trigger === "hover" ? this.openDropdown : undefined}
85
+ onMouseLeave={this.config.trigger === "hover" ? this.closeDropdown : undefined}
86
+ disabled={this.config.disabled}
87
+ aria-expanded={this.isOpen}
88
+ aria-haspopup="listbox"
89
+ >
90
+ <span class="dropdown-text">{displayText}</span>
91
+ <span class="dropdown-arrow">{this.isOpen ? "▲" : "▼"}</span>
92
+ </button>
93
+
94
+ {this.isOpen && (
95
+ <div
96
+ ref={(el) => (this.dropdownElement = el)}
97
+ class={`dropdown-menu dropdown-${this.config.position} dropdown-${this.config.align}`}
98
+ role="listbox"
99
+ onMouseEnter={
100
+ this.config.trigger === "hover" ? this.openDropdown : undefined
101
+ }
102
+ onMouseLeave={
103
+ this.config.trigger === "hover" ? this.closeDropdown : undefined
104
+ }
105
+ >
106
+ {this.options.length === 0 ? (
107
+ <div class="dropdown-empty">No options</div>
108
+ ) : (
109
+ this.options.map((option) => (
110
+ <button
111
+ key={option.value}
112
+ class={`dropdown-option ${
113
+ option.value === this.selectedValue &&
114
+ this.selectedValue !== ""
115
+ ? "selected"
116
+ : ""
117
+ } ${option.disabled ? "disabled" : ""}`}
118
+ onClick={() =>
119
+ !option.disabled && this.selectOption(option.value)
120
+ }
121
+ role="option"
122
+ aria-selected={
123
+ option.value === this.selectedValue &&
124
+ this.selectedValue !== ""
125
+ }
126
+ disabled={option.disabled}
127
+ >
128
+ {option.render ? option.render() : <span>{option.label}</span>}
129
+ </button>
130
+ ))
131
+ )}
132
+ </div>
133
+ )}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ /**
139
+ * 切换下拉菜单
140
+ */
141
+ private toggleDropdown = (): void => {
142
+ if (this.config.disabled) return;
143
+ this.isOpen = !this.isOpen;
144
+ this.rerender();
145
+
146
+ if (this.isOpen) {
147
+ setTimeout(() => {
148
+ this.attachOutsideClickHandler();
149
+ }, 0);
150
+ } else {
151
+ this.detachOutsideClickHandler();
152
+ }
153
+ };
154
+
155
+ /**
156
+ * 打开下拉菜单
157
+ */
158
+ private openDropdown = (): void => {
159
+ if (this.config.disabled || this.isOpen) return;
160
+ this.isOpen = true;
161
+ this.rerender();
162
+ };
163
+
164
+ /**
165
+ * 关闭下拉菜单
166
+ */
167
+ private closeDropdown = (): void => {
168
+ if (!this.isOpen) return;
169
+ this.isOpen = false;
170
+ this.rerender();
171
+ this.detachOutsideClickHandler();
172
+ };
173
+
174
+ /**
175
+ * 选择选项
176
+ */
177
+ private selectOption = (value: string): void => {
178
+ this.setValue(value);
179
+ if (this.config.trigger === "click") {
180
+ this.closeDropdown();
181
+ }
182
+ };
183
+
184
+ /**
185
+ * 附加外部点击处理器
186
+ */
187
+ private attachOutsideClickHandler = (): void => {
188
+ if (this.config.trigger === "hover") return;
189
+
190
+ this.outsideClickHandler = (event: Event) => {
191
+ const target = event.target as Node;
192
+ if (
193
+ this.dropdownElement &&
194
+ this.buttonElement &&
195
+ !this.dropdownElement.contains(target) &&
196
+ !this.buttonElement.contains(target)
197
+ ) {
198
+ this.closeDropdown();
199
+ }
200
+ };
201
+
202
+ document.addEventListener("click", this.outsideClickHandler, true);
203
+ };
204
+
205
+ /**
206
+ * 移除外部点击处理器
207
+ */
208
+ private detachOutsideClickHandler = (): void => {
209
+ if (this.outsideClickHandler) {
210
+ document.removeEventListener("click", this.outsideClickHandler, true);
211
+ this.outsideClickHandler = undefined;
212
+ }
213
+ };
214
+
215
+ /**
216
+ * 组件断开连接时清理
217
+ */
218
+ protected onDisconnected(): void {
219
+ this.detachOutsideClickHandler();
220
+ }
221
+ }
@@ -0,0 +1,148 @@
1
+ /* LanguageSwitcher Component Styles */
2
+
3
+ :host {
4
+ display: block;
5
+ position: relative;
6
+ }
7
+
8
+ .language-switcher-container {
9
+ position: relative;
10
+ display: inline-block;
11
+ }
12
+
13
+ .language-switcher-btn {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 0.5rem;
17
+ padding: 0.5rem 0.75rem;
18
+ background: var(--language-switcher-bg, rgba(255, 255, 255, 0.1));
19
+ border: 1px solid var(--language-switcher-border, rgba(255, 255, 255, 0.2));
20
+ border-radius: var(--language-switcher-border-radius, 0.5rem);
21
+ color: var(--language-switcher-color, currentColor);
22
+ cursor: pointer;
23
+ font-size: 0.875rem;
24
+ font-weight: 500;
25
+ transition: all 0.2s ease;
26
+ min-width: 100px;
27
+ }
28
+
29
+ .language-switcher-btn:hover {
30
+ background: var(--language-switcher-hover-bg, rgba(255, 255, 255, 0.15));
31
+ border-color: var(--language-switcher-hover-border, rgba(255, 255, 255, 0.3));
32
+ }
33
+
34
+ .language-switcher-btn:focus {
35
+ outline: 2px solid var(--language-switcher-focus-color, var(--focus-color, #dc2626));
36
+ outline-offset: 2px;
37
+ }
38
+
39
+ .language-switcher-icon {
40
+ font-size: 1rem;
41
+ line-height: 1;
42
+ }
43
+
44
+ .language-switcher-text {
45
+ flex: 1;
46
+ text-align: left;
47
+ white-space: nowrap;
48
+ }
49
+
50
+ .language-switcher-arrow {
51
+ font-size: 0.625rem;
52
+ opacity: 0.7;
53
+ transition: transform 0.2s ease;
54
+ }
55
+
56
+ .language-switcher-dropdown {
57
+ position: absolute;
58
+ top: calc(100% + 0.25rem);
59
+ right: 0;
60
+ min-width: 160px;
61
+ max-height: 300px;
62
+ overflow-y: auto;
63
+ background: var(--language-switcher-dropdown-bg, #ffffff);
64
+ border: 1px solid var(--language-switcher-dropdown-border, #e5e7eb);
65
+ border-radius: var(--language-switcher-border-radius, 0.5rem);
66
+ box-shadow:
67
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
68
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
69
+ z-index: 1000;
70
+ padding: 0.25rem;
71
+ margin: 0;
72
+ }
73
+
74
+ [data-theme="dark"] .language-switcher-dropdown {
75
+ background: var(--language-switcher-dropdown-bg-dark, #1f2937);
76
+ border-color: var(--language-switcher-dropdown-border-dark, #374151);
77
+ }
78
+
79
+ .language-switcher-option {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ width: 100%;
84
+ padding: 0.5rem 0.75rem;
85
+ background: transparent;
86
+ border: none;
87
+ border-radius: 0.375rem;
88
+ color: var(--language-switcher-option-color, #1f2937);
89
+ cursor: pointer;
90
+ font-size: 0.875rem;
91
+ text-align: left;
92
+ transition: background-color 0.15s ease;
93
+ }
94
+
95
+ [data-theme="dark"] .language-switcher-option {
96
+ color: var(--language-switcher-option-color-dark, #f3f4f6);
97
+ }
98
+
99
+ .language-switcher-option:hover {
100
+ background: var(--language-switcher-option-hover-bg, #f3f4f6);
101
+ }
102
+
103
+ [data-theme="dark"] .language-switcher-option:hover {
104
+ background: var(--language-switcher-option-hover-bg-dark, #374151);
105
+ }
106
+
107
+ .language-switcher-option.active {
108
+ background: var(--language-switcher-option-active-bg, #dc2626);
109
+ color: var(--language-switcher-option-active-color, #ffffff);
110
+ font-weight: 600;
111
+ }
112
+
113
+ .language-switcher-option.active:hover {
114
+ background: var(--language-switcher-option-active-hover-bg, #b91c1c);
115
+ }
116
+
117
+ .language-name {
118
+ flex: 1;
119
+ }
120
+
121
+ .language-code {
122
+ font-size: 0.75rem;
123
+ opacity: 0.7;
124
+ margin-left: 0.5rem;
125
+ font-weight: 500;
126
+ }
127
+
128
+ .language-switcher-option.active .language-code {
129
+ opacity: 0.9;
130
+ }
131
+
132
+ /* Responsive design */
133
+ @media (max-width: 768px) {
134
+ .language-switcher-btn {
135
+ min-width: 80px;
136
+ padding: 0.375rem 0.5rem;
137
+ font-size: 0.8125rem;
138
+ }
139
+
140
+ .language-switcher-text {
141
+ display: none;
142
+ }
143
+
144
+ .language-switcher-dropdown {
145
+ right: 0;
146
+ min-width: 140px;
147
+ }
148
+ }
@@ -0,0 +1,190 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * LanguageSwitcher Component
4
+ * 语言切换器组件,用于切换 i18next 的语言
5
+ * 不使用国旗图标,使用语言名称显示
6
+ */
7
+ import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
8
+ import { i18nInstance } from "@wsxjs/wsx-i18next";
9
+ import styles from "./LanguageSwitcher.css?inline";
10
+
11
+ /**
12
+ * 语言选项接口
13
+ */
14
+ export interface LanguageOption {
15
+ /** 语言代码(如 'en', 'zh') */
16
+ code: string;
17
+ /** 语言显示名称(如 'English', '中文') */
18
+ name: string;
19
+ }
20
+
21
+ @autoRegister({ tagName: "language-switcher" })
22
+ export default class LanguageSwitcher extends WebComponent {
23
+ /** 支持的语言列表 */
24
+ private languages: LanguageOption[] = [
25
+ { code: "en", name: "English" },
26
+ { code: "zh", name: "中文" },
27
+ { code: "es", name: "Español" },
28
+ { code: "fr", name: "Français" },
29
+ { code: "de", name: "Deutsch" },
30
+ { code: "ja", name: "日本語" },
31
+ { code: "ko", name: "한국어" },
32
+ ];
33
+
34
+ /** 当前选中的语言代码 */
35
+ @state private currentLanguage: string = "en";
36
+
37
+ /** 下拉菜单是否打开 */
38
+ @state private isOpen: boolean = false;
39
+
40
+ /** 下拉菜单元素引用 */
41
+ private dropdownElement?: HTMLElement;
42
+ private buttonElement?: HTMLElement;
43
+
44
+ constructor() {
45
+ super({
46
+ styles,
47
+ styleName: "language-switcher",
48
+ });
49
+ }
50
+
51
+ render(): HTMLElement {
52
+ const currentLang = this.languages.find((lang) => lang.code === this.currentLanguage);
53
+ const displayName = currentLang?.name || this.currentLanguage.toUpperCase();
54
+
55
+ return (
56
+ <div class="language-switcher-container">
57
+ <button
58
+ ref={(el) => (this.buttonElement = el)}
59
+ class="language-switcher-btn"
60
+ onClick={this.toggleDropdown}
61
+ aria-label={`Current language: ${displayName}`}
62
+ aria-expanded={this.isOpen}
63
+ aria-haspopup="listbox"
64
+ >
65
+ <span class="language-switcher-icon">🌐</span>
66
+ <span class="language-switcher-text">{displayName}</span>
67
+ <span class="language-switcher-arrow">{this.isOpen ? "▲" : "▼"}</span>
68
+ </button>
69
+
70
+ {this.isOpen && (
71
+ <div
72
+ ref={(el) => (this.dropdownElement = el)}
73
+ class="language-switcher-dropdown"
74
+ role="listbox"
75
+ >
76
+ {this.languages.map((lang) => (
77
+ <button
78
+ key={lang.code}
79
+ class={`language-switcher-option ${
80
+ lang.code === this.currentLanguage ? "active" : ""
81
+ }`}
82
+ onClick={() => this.selectLanguage(lang.code)}
83
+ role="option"
84
+ aria-selected={lang.code === this.currentLanguage}
85
+ >
86
+ <span class="language-name">{lang.name}</span>
87
+ <span class="language-code">{lang.code.toUpperCase()}</span>
88
+ </button>
89
+ ))}
90
+ </div>
91
+ )}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * 切换下拉菜单
98
+ */
99
+ private toggleDropdown = (): void => {
100
+ this.isOpen = !this.isOpen;
101
+ this.rerender();
102
+
103
+ if (this.isOpen) {
104
+ // 点击外部关闭下拉菜单
105
+ setTimeout(() => {
106
+ document.addEventListener("click", this.handleOutsideClick, true);
107
+ }, 0);
108
+ }
109
+ };
110
+
111
+ /**
112
+ * 处理点击外部区域
113
+ */
114
+ private handleOutsideClick = (event: Event): void => {
115
+ const target = event.target as Node;
116
+ if (
117
+ this.dropdownElement &&
118
+ this.buttonElement &&
119
+ !this.dropdownElement.contains(target) &&
120
+ !this.buttonElement.contains(target)
121
+ ) {
122
+ this.isOpen = false;
123
+ this.rerender();
124
+ document.removeEventListener("click", this.handleOutsideClick, true);
125
+ }
126
+ };
127
+
128
+ /**
129
+ * 选择语言
130
+ */
131
+ private selectLanguage = (languageCode: string): void => {
132
+ if (languageCode === this.currentLanguage) {
133
+ this.isOpen = false;
134
+ this.rerender();
135
+ return;
136
+ }
137
+
138
+ // 更改 i18next 语言
139
+ i18nInstance.changeLanguage(languageCode).then(() => {
140
+ this.currentLanguage = languageCode;
141
+ this.isOpen = false;
142
+ this.rerender();
143
+
144
+ // 保存到 localStorage
145
+ localStorage.setItem("wsx-language", languageCode);
146
+ });
147
+ };
148
+
149
+ /**
150
+ * 组件连接时初始化
151
+ */
152
+ protected onConnected(): void {
153
+ // 从 localStorage 或 i18next 获取当前语言
154
+ const savedLanguage = localStorage.getItem("wsx-language");
155
+ const i18nLanguage = i18nInstance.language || i18nInstance.options.fallbackLng || "en";
156
+
157
+ // 获取基础语言代码(处理 'en-US' -> 'en' 的情况)
158
+ const baseLanguage = (savedLanguage || i18nLanguage).split("-")[0];
159
+
160
+ if (baseLanguage !== this.currentLanguage) {
161
+ this.currentLanguage = baseLanguage;
162
+ // 如果 localStorage 中的语言与 i18next 不一致,更新 i18next
163
+ if (savedLanguage && savedLanguage !== i18nLanguage) {
164
+ i18nInstance.changeLanguage(savedLanguage);
165
+ }
166
+ }
167
+
168
+ // 监听 i18next 语言变化事件
169
+ i18nInstance.on("languageChanged", this.handleLanguageChanged);
170
+ }
171
+
172
+ /**
173
+ * 组件断开连接时清理
174
+ */
175
+ protected onDisconnected(): void {
176
+ i18nInstance.off("languageChanged", this.handleLanguageChanged);
177
+ document.removeEventListener("click", this.handleOutsideClick, true);
178
+ }
179
+
180
+ /**
181
+ * 处理 i18next 语言变化事件
182
+ */
183
+ private handleLanguageChanged = (lng: string): void => {
184
+ const baseLanguage = lng.split("-")[0];
185
+ if (baseLanguage !== this.currentLanguage) {
186
+ this.currentLanguage = baseLanguage;
187
+ this.rerender();
188
+ }
189
+ };
190
+ }