@wsxjs/wsx-base-components 0.0.16 → 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,352 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * Combobox Component
4
+ * 组合框组件,结合输入框和下拉菜单
5
+ * 支持搜索、过滤、选择等功能
6
+ */
7
+ import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
8
+ import styles from "./Combobox.css?inline";
9
+ import type { DropdownOption } from "./Dropdown.types";
10
+ import type { ComboboxOption, ComboboxConfig } from "./Combobox.types";
11
+ export type { ComboboxOption, ComboboxConfig };
12
+
13
+ @autoRegister({ tagName: "wsx-combobox" })
14
+ export default class Combobox extends WebComponent {
15
+ /** 选项列表 */
16
+ private options: DropdownOption[] = [];
17
+ /** 当前选中的值(单选)或值数组(多选) */
18
+ @state private selectedValue: string | string[] = "";
19
+ /** 搜索关键词 */
20
+ @state private searchQuery: string = "";
21
+ /** 下拉菜单是否打开 */
22
+ @state private isOpen: boolean = false;
23
+ /** 配置选项 */
24
+ private config: ComboboxConfig = {};
25
+
26
+ /** 元素引用 */
27
+ private inputElement?: HTMLInputElement;
28
+ private dropdownElement?: HTMLElement;
29
+ private outsideClickHandler?: (event: Event) => void;
30
+
31
+ constructor(config: ComboboxConfig = {}) {
32
+ super({
33
+ styles,
34
+ styleName: "wsx-combobox",
35
+ });
36
+ this.config = {
37
+ placeholder: "Select or search...",
38
+ searchable: true,
39
+ multiple: false,
40
+ align: "left",
41
+ position: "bottom",
42
+ ...config,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * 设置选项列表
48
+ */
49
+ public setOptions(options: DropdownOption[]): void {
50
+ this.options = options;
51
+ this.rerender();
52
+ }
53
+
54
+ /**
55
+ * 设置选中的值
56
+ */
57
+ public setValue(value: string | string[]): void {
58
+ this.selectedValue = value;
59
+ if (this.inputElement && !this.config.multiple) {
60
+ const selectedOption = this.options.find(
61
+ (opt) => opt.value === (Array.isArray(value) ? value[0] : value)
62
+ );
63
+ this.inputElement.value = selectedOption?.label || "";
64
+ }
65
+ this.rerender();
66
+ this.dispatchEvent(
67
+ new CustomEvent("change", {
68
+ detail: { value },
69
+ bubbles: true,
70
+ })
71
+ );
72
+ }
73
+
74
+ /**
75
+ * 获取选中的值
76
+ */
77
+ public getValue(): string | string[] {
78
+ return this.selectedValue;
79
+ }
80
+
81
+ /**
82
+ * 获取过滤后的选项列表
83
+ */
84
+ private getFilteredOptions(): DropdownOption[] {
85
+ if (!this.config.searchable || !this.searchQuery.trim()) {
86
+ return this.options;
87
+ }
88
+
89
+ const query = this.searchQuery.toLowerCase();
90
+ return this.options.filter(
91
+ (opt) =>
92
+ opt.label.toLowerCase().includes(query) || opt.value.toLowerCase().includes(query)
93
+ );
94
+ }
95
+
96
+ render(): HTMLElement {
97
+ const filteredOptions = this.getFilteredOptions();
98
+ const isMultiple = this.config.multiple;
99
+ const selectedValues = Array.isArray(this.selectedValue)
100
+ ? this.selectedValue
101
+ : this.selectedValue
102
+ ? [this.selectedValue]
103
+ : [];
104
+
105
+ // 获取显示文本
106
+ let displayText = "";
107
+ if (isMultiple) {
108
+ if (selectedValues.length === 0) {
109
+ displayText = this.config.placeholder || "Select...";
110
+ } else if (selectedValues.length === 1) {
111
+ const option = this.options.find((opt) => opt.value === selectedValues[0]);
112
+ displayText = option?.label || selectedValues[0];
113
+ } else {
114
+ displayText = `${selectedValues.length} selected`;
115
+ }
116
+ } else {
117
+ const selectedOption = this.options.find((opt) => opt.value === this.selectedValue);
118
+ displayText = selectedOption?.label || this.config.placeholder || "Select...";
119
+ }
120
+
121
+ return (
122
+ <div class="combobox-container">
123
+ <div class="combobox-input-wrapper">
124
+ {isMultiple && selectedValues.length > 0 && (
125
+ <div class="combobox-tags">
126
+ {selectedValues.slice(0, 2).map((value) => {
127
+ const option = this.options.find((opt) => opt.value === value);
128
+ return (
129
+ <span key={value} class="combobox-tag">
130
+ {option?.label || value}
131
+ <button
132
+ class="combobox-tag-remove"
133
+ onClick={(e) => {
134
+ e.stopPropagation();
135
+ this.removeValue(value);
136
+ }}
137
+ >
138
+ ×
139
+ </button>
140
+ </span>
141
+ );
142
+ })}
143
+ {selectedValues.length > 2 && (
144
+ <span class="combobox-tag-more">+{selectedValues.length - 2}</span>
145
+ )}
146
+ </div>
147
+ )}
148
+ <input
149
+ ref={(el) => (this.inputElement = el)}
150
+ type="text"
151
+ class="combobox-input"
152
+ placeholder={displayText}
153
+ value={this.config.searchable && this.isOpen ? this.searchQuery : ""}
154
+ onInput={this.handleInput}
155
+ onFocus={this.openDropdown}
156
+ onClick={this.openDropdown}
157
+ disabled={this.config.disabled}
158
+ readonly={!this.config.searchable}
159
+ />
160
+ <button
161
+ class={`combobox-arrow ${this.isOpen ? "open" : ""}`}
162
+ onClick={this.toggleDropdown}
163
+ disabled={this.config.disabled}
164
+ >
165
+ {this.isOpen ? "▲" : "▼"}
166
+ </button>
167
+ </div>
168
+
169
+ {this.isOpen && (
170
+ <div
171
+ ref={(el) => (this.dropdownElement = el)}
172
+ class={`combobox-menu combobox-${this.config.position} combobox-${this.config.align}`}
173
+ role="listbox"
174
+ >
175
+ {filteredOptions.length === 0 ? (
176
+ <div class="combobox-empty">
177
+ {this.config.searchable && this.searchQuery
178
+ ? "No results found"
179
+ : "No options"}
180
+ </div>
181
+ ) : (
182
+ filteredOptions.map((option) => {
183
+ const isSelected = isMultiple
184
+ ? selectedValues.includes(option.value)
185
+ : option.value === this.selectedValue;
186
+
187
+ return (
188
+ <button
189
+ key={option.value}
190
+ class={`combobox-option ${isSelected ? "selected" : ""} ${
191
+ option.disabled ? "disabled" : ""
192
+ }`}
193
+ onClick={() =>
194
+ !option.disabled && this.selectOption(option.value)
195
+ }
196
+ role="option"
197
+ aria-selected={isSelected}
198
+ disabled={option.disabled}
199
+ >
200
+ {isMultiple && (
201
+ <span class="combobox-checkbox">
202
+ {isSelected ? "✓" : ""}
203
+ </span>
204
+ )}
205
+ {option.render ? (
206
+ option.render()
207
+ ) : (
208
+ <span>{option.label}</span>
209
+ )}
210
+ </button>
211
+ );
212
+ })
213
+ )}
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ /**
221
+ * 处理输入
222
+ */
223
+ private handleInput = (event: Event): void => {
224
+ if (!this.config.searchable) return;
225
+ const input = event.target as HTMLInputElement;
226
+ this.searchQuery = input.value;
227
+ this.rerender();
228
+ };
229
+
230
+ /**
231
+ * 切换下拉菜单
232
+ */
233
+ private toggleDropdown = (): void => {
234
+ if (this.config.disabled) return;
235
+ this.isOpen = !this.isOpen;
236
+ if (this.isOpen && this.config.searchable && this.inputElement) {
237
+ this.inputElement.focus();
238
+ }
239
+ this.rerender();
240
+
241
+ if (this.isOpen) {
242
+ setTimeout(() => {
243
+ this.attachOutsideClickHandler();
244
+ }, 0);
245
+ } else {
246
+ this.detachOutsideClickHandler();
247
+ this.searchQuery = "";
248
+ }
249
+ };
250
+
251
+ /**
252
+ * 打开下拉菜单
253
+ */
254
+ private openDropdown = (): void => {
255
+ if (this.config.disabled || this.isOpen) return;
256
+ this.isOpen = true;
257
+ if (this.config.searchable && this.inputElement) {
258
+ this.inputElement.focus();
259
+ }
260
+ this.rerender();
261
+ setTimeout(() => {
262
+ this.attachOutsideClickHandler();
263
+ }, 0);
264
+ };
265
+
266
+ /**
267
+ * 关闭下拉菜单
268
+ */
269
+ private closeDropdown = (): void => {
270
+ if (!this.isOpen) return;
271
+ this.isOpen = false;
272
+ this.searchQuery = "";
273
+ this.rerender();
274
+ this.detachOutsideClickHandler();
275
+ };
276
+
277
+ /**
278
+ * 选择选项
279
+ */
280
+ private selectOption = (value: string): void => {
281
+ if (this.config.multiple) {
282
+ const currentValues = Array.isArray(this.selectedValue) ? this.selectedValue : [];
283
+ const newValues = currentValues.includes(value)
284
+ ? currentValues.filter((v) => v !== value)
285
+ : [...currentValues, value];
286
+ this.selectedValue = newValues;
287
+ } else {
288
+ this.selectedValue = value;
289
+ this.closeDropdown();
290
+ }
291
+
292
+ this.rerender();
293
+ this.dispatchEvent(
294
+ new CustomEvent("change", {
295
+ detail: { value: this.selectedValue },
296
+ bubbles: true,
297
+ })
298
+ );
299
+ };
300
+
301
+ /**
302
+ * 移除值(多选模式)
303
+ */
304
+ private removeValue = (value: string): void => {
305
+ if (!this.config.multiple) return;
306
+ const currentValues = Array.isArray(this.selectedValue) ? this.selectedValue : [];
307
+ this.selectedValue = currentValues.filter((v) => v !== value);
308
+ this.rerender();
309
+ this.dispatchEvent(
310
+ new CustomEvent("change", {
311
+ detail: { value: this.selectedValue },
312
+ bubbles: true,
313
+ })
314
+ );
315
+ };
316
+
317
+ /**
318
+ * 附加外部点击处理器
319
+ */
320
+ private attachOutsideClickHandler = (): void => {
321
+ this.outsideClickHandler = (event: Event) => {
322
+ const target = event.target as Node;
323
+ if (
324
+ this.dropdownElement &&
325
+ this.inputElement &&
326
+ !this.dropdownElement.contains(target) &&
327
+ !this.inputElement.contains(target)
328
+ ) {
329
+ this.closeDropdown();
330
+ }
331
+ };
332
+
333
+ document.addEventListener("click", this.outsideClickHandler, true);
334
+ };
335
+
336
+ /**
337
+ * 移除外部点击处理器
338
+ */
339
+ private detachOutsideClickHandler = (): void => {
340
+ if (this.outsideClickHandler) {
341
+ document.removeEventListener("click", this.outsideClickHandler, true);
342
+ this.outsideClickHandler = undefined;
343
+ }
344
+ };
345
+
346
+ /**
347
+ * 组件断开连接时清理
348
+ */
349
+ protected onDisconnected(): void {
350
+ this.detachOutsideClickHandler();
351
+ }
352
+ }
@@ -0,0 +1,178 @@
1
+ /* Dropdown Component Styles */
2
+
3
+ :host {
4
+ display: inline-block;
5
+ position: relative;
6
+ }
7
+
8
+ .dropdown-container {
9
+ position: relative;
10
+ display: inline-block;
11
+ }
12
+
13
+ .dropdown-button {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: 0.5rem;
18
+ padding: 0.5rem 0.75rem;
19
+ min-width: 120px;
20
+ background: var(--dropdown-bg, rgba(255, 255, 255, 0.1));
21
+ border: 1px solid var(--dropdown-border, rgba(255, 255, 255, 0.2));
22
+ border-radius: var(--dropdown-border-radius, 0.5rem);
23
+ color: var(--dropdown-color, currentColor);
24
+ cursor: pointer;
25
+ font-size: 0.875rem;
26
+ font-weight: 500;
27
+ transition: all 0.2s ease;
28
+ text-align: left;
29
+ }
30
+
31
+ .dropdown-button:hover:not(.disabled) {
32
+ background: var(--dropdown-hover-bg, rgba(255, 255, 255, 0.15));
33
+ border-color: var(--dropdown-hover-border, rgba(255, 255, 255, 0.3));
34
+ }
35
+
36
+ .dropdown-button:focus:not(.disabled) {
37
+ outline: 2px solid var(--dropdown-focus-color, var(--focus-color, #dc2626));
38
+ outline-offset: 2px;
39
+ }
40
+
41
+ .dropdown-button.open {
42
+ background: var(--dropdown-open-bg, rgba(255, 255, 255, 0.2));
43
+ }
44
+
45
+ .dropdown-button.disabled {
46
+ opacity: 0.5;
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ .dropdown-text {
51
+ flex: 1;
52
+ overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ white-space: nowrap;
55
+ }
56
+
57
+ .dropdown-arrow {
58
+ font-size: 0.625rem;
59
+ opacity: 0.7;
60
+ transition: transform 0.2s ease;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .dropdown-button.open .dropdown-arrow {
65
+ transform: rotate(180deg);
66
+ }
67
+
68
+ .dropdown-menu {
69
+ position: absolute;
70
+ z-index: 1000;
71
+ min-width: 160px;
72
+ max-width: 300px;
73
+ max-height: 300px;
74
+ overflow-y: auto;
75
+ background: var(--dropdown-menu-bg, #ffffff);
76
+ border: 1px solid var(--dropdown-menu-border, #e5e7eb);
77
+ border-radius: var(--dropdown-border-radius, 0.5rem);
78
+ box-shadow:
79
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
80
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
81
+ padding: 0.25rem;
82
+ margin: 0.25rem 0 0 0;
83
+ }
84
+
85
+ [data-theme="dark"] .dropdown-menu {
86
+ background: var(--dropdown-menu-bg-dark, #1f2937);
87
+ border-color: var(--dropdown-menu-border-dark, #374151);
88
+ }
89
+
90
+ .dropdown-menu.dropdown-top {
91
+ bottom: 100%;
92
+ margin: 0 0 0.25rem 0;
93
+ }
94
+
95
+ .dropdown-menu.dropdown-bottom {
96
+ top: 100%;
97
+ margin: 0.25rem 0 0 0;
98
+ }
99
+
100
+ .dropdown-menu.dropdown-left {
101
+ left: 0;
102
+ }
103
+
104
+ .dropdown-menu.dropdown-right {
105
+ right: 0;
106
+ }
107
+
108
+ .dropdown-menu.dropdown-center {
109
+ left: 50%;
110
+ transform: translateX(-50%);
111
+ }
112
+
113
+ .dropdown-menu.dropdown-top.dropdown-center {
114
+ transform: translateX(-50%) translateY(-100%);
115
+ }
116
+
117
+ .dropdown-option {
118
+ display: flex;
119
+ align-items: center;
120
+ width: 100%;
121
+ padding: 0.5rem 0.75rem;
122
+ background: transparent;
123
+ border: none;
124
+ border-radius: 0.375rem;
125
+ color: var(--dropdown-option-color, #1f2937);
126
+ cursor: pointer;
127
+ font-size: 0.875rem;
128
+ text-align: left;
129
+ transition: background-color 0.15s ease;
130
+ }
131
+
132
+ [data-theme="dark"] .dropdown-option {
133
+ color: var(--dropdown-option-color-dark, #f3f4f6);
134
+ }
135
+
136
+ .dropdown-option:hover:not(.disabled) {
137
+ background: var(--dropdown-option-hover-bg, #f3f4f6);
138
+ }
139
+
140
+ [data-theme="dark"] .dropdown-option:hover:not(.disabled) {
141
+ background: var(--dropdown-option-hover-bg-dark, #374151);
142
+ }
143
+
144
+ .dropdown-option.selected {
145
+ background: var(--dropdown-option-selected-bg, #dc2626);
146
+ color: var(--dropdown-option-selected-color, #ffffff);
147
+ font-weight: 600;
148
+ }
149
+
150
+ .dropdown-option.selected:hover {
151
+ background: var(--dropdown-option-selected-hover-bg, #b91c1c);
152
+ }
153
+
154
+ .dropdown-option.disabled {
155
+ opacity: 0.5;
156
+ cursor: not-allowed;
157
+ }
158
+
159
+ .dropdown-empty {
160
+ padding: 0.5rem 0.75rem;
161
+ color: var(--dropdown-empty-color, #9ca3af);
162
+ font-size: 0.875rem;
163
+ text-align: center;
164
+ }
165
+
166
+ /* Responsive design */
167
+ @media (max-width: 768px) {
168
+ .dropdown-button {
169
+ min-width: 100px;
170
+ padding: 0.375rem 0.5rem;
171
+ font-size: 0.8125rem;
172
+ }
173
+
174
+ .dropdown-menu {
175
+ min-width: 140px;
176
+ max-width: 200px;
177
+ }
178
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Dropdown Types
3
+ * 下拉菜单组件的类型定义
4
+ */
5
+
6
+ export interface DropdownOption {
7
+ /** 选项值 */
8
+ value: string;
9
+ /** 选项标签 */
10
+ label: string;
11
+ /** 是否禁用 */
12
+ disabled?: boolean;
13
+ /** 自定义渲染函数 */
14
+ render?: () => HTMLElement;
15
+ }
16
+
17
+ export interface DropdownConfig {
18
+ /** 占位符文本 */
19
+ placeholder?: string;
20
+ /** 是否禁用 */
21
+ disabled?: boolean;
22
+ /** 下拉菜单位置(top/bottom) */
23
+ position?: "top" | "bottom";
24
+ /** 下拉菜单对齐方式(left/right/center) */
25
+ align?: "left" | "right" | "center";
26
+ /** 触发方式(click/hover) */
27
+ trigger?: "click" | "hover";
28
+ }