@wsxjs/wsx-base-components 0.0.17 → 0.0.19
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/LICENSE +2 -2
- package/README.md +28 -28
- package/dist/index.cjs +14 -2
- package/dist/index.js +4971 -2032
- package/dist/style.css +1 -1
- package/package.json +16 -7
- package/src/{XyButton.css → Button.css} +1 -1
- package/src/{XyButton.wsx → Button.wsx} +18 -9
- package/src/ButtonGroup.css +30 -0
- package/src/{XyButtonGroup.wsx → ButtonGroup.wsx} +26 -14
- package/src/CodeBlock.css +275 -0
- package/src/CodeBlock.types.ts +25 -0
- package/src/CodeBlock.wsx +296 -0
- package/src/ColorPicker.wsx +6 -5
- package/src/Combobox.css +254 -0
- package/src/Combobox.types.ts +32 -0
- package/src/Combobox.wsx +352 -0
- package/src/Dropdown.css +178 -0
- package/src/Dropdown.types.ts +28 -0
- package/src/Dropdown.wsx +221 -0
- package/src/LanguageSwitcher.css +148 -0
- package/src/LanguageSwitcher.wsx +190 -0
- package/src/OverflowDetector.ts +169 -0
- package/src/ResponsiveNav.css +555 -0
- package/src/ResponsiveNav.types.ts +30 -0
- package/src/ResponsiveNav.wsx +450 -0
- package/src/SvgIcon.wsx +2 -2
- package/src/index.ts +17 -9
- package/src/types/wsx.d.ts +4 -3
- package/src/ReactiveCounter.css +0 -304
- package/src/ReactiveCounter.wsx +0 -231
- package/src/SimpleReactiveDemo.wsx +0 -59
- package/src/SvgDemo.wsx +0 -241
- package/src/TodoList.css +0 -197
- package/src/TodoList.wsx +0 -264
- package/src/TodoListLight.css +0 -198
- package/src/TodoListLight.wsx +0 -263
- package/src/UserProfile.css +0 -146
- package/src/UserProfile.wsx +0 -247
- package/src/UserProfileLight.css +0 -146
- package/src/UserProfileLight.wsx +0 -256
- package/src/XyButtonGroup.css +0 -30
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* Code Block Component
|
|
4
|
+
* 通用的代码展示组件,支持复制功能和自定义操作按钮
|
|
5
|
+
*/
|
|
6
|
+
import { LightComponent, autoRegister, state } from "@wsxjs/wsx-core";
|
|
7
|
+
import type { CodeSegment, CodeBlockConfig } from "./CodeBlock.types";
|
|
8
|
+
import styles from "./CodeBlock.css?inline";
|
|
9
|
+
// 导入 Prism.js 主题 CSS
|
|
10
|
+
import "prismjs/themes/prism-tomorrow.css";
|
|
11
|
+
// 静态导入 Prism.js 核心
|
|
12
|
+
import Prism from "prismjs";
|
|
13
|
+
// 静态导入所有支持的语言(按依赖顺序)
|
|
14
|
+
// 基础语言(无依赖)
|
|
15
|
+
import "prismjs/components/prism-markup";
|
|
16
|
+
import "prismjs/components/prism-css";
|
|
17
|
+
import "prismjs/components/prism-javascript";
|
|
18
|
+
import "prismjs/components/prism-json";
|
|
19
|
+
import "prismjs/components/prism-bash";
|
|
20
|
+
import "prismjs/components/prism-sql";
|
|
21
|
+
import "prismjs/components/prism-yaml";
|
|
22
|
+
import "prismjs/components/prism-c"; // C 语言是基础语言
|
|
23
|
+
import "prismjs/components/prism-markdown";
|
|
24
|
+
import "prismjs/components/prism-python";
|
|
25
|
+
import "prismjs/components/prism-java";
|
|
26
|
+
import "prismjs/components/prism-rust";
|
|
27
|
+
import "prismjs/components/prism-go";
|
|
28
|
+
// 依赖基础语言的语言
|
|
29
|
+
import "prismjs/components/prism-typescript"; // 依赖 javascript
|
|
30
|
+
import "prismjs/components/prism-jsx"; // 依赖 javascript
|
|
31
|
+
import "prismjs/components/prism-cpp"; // 依赖 c
|
|
32
|
+
// 依赖其他语言的语言(必须在依赖之后)
|
|
33
|
+
import "prismjs/components/prism-tsx"; // 依赖 typescript 和 jsx
|
|
34
|
+
|
|
35
|
+
@autoRegister({ tagName: "code-block" })
|
|
36
|
+
export default class CodeBlock extends LightComponent {
|
|
37
|
+
static get observedAttributes(): string[] {
|
|
38
|
+
return ["code", "title", "language", "show-copy", "show-try-online", "try-online-url"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@state private copied: boolean = false;
|
|
42
|
+
@state private highlighted: boolean = false;
|
|
43
|
+
@state private code: string = "";
|
|
44
|
+
@state private segments: CodeSegment[] = [];
|
|
45
|
+
@state private codeTitle: string = "";
|
|
46
|
+
@state private language: string = "typescript";
|
|
47
|
+
@state private showCopy: boolean = true;
|
|
48
|
+
@state private showTryOnline: boolean = false;
|
|
49
|
+
@state private tryOnlineUrl: string = "";
|
|
50
|
+
private onTryOnlineCallback?: () => void;
|
|
51
|
+
private codeElements: HTMLElement[] = [];
|
|
52
|
+
private isHighlighting: boolean = false; // 防止重复高亮
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
super({ styles });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected onConnected() {
|
|
59
|
+
// 从属性初始化(使用 @state 会自动触发重渲染)
|
|
60
|
+
this.code = this.getAttribute("code") || "";
|
|
61
|
+
this.codeTitle = this.getAttribute("title") || "";
|
|
62
|
+
this.language = this.getAttribute("language") || "typescript";
|
|
63
|
+
this.showCopy = this.getAttribute("show-copy") !== "false";
|
|
64
|
+
this.showTryOnline = this.getAttribute("show-try-online") === "true";
|
|
65
|
+
this.tryOnlineUrl = this.getAttribute("try-online-url") || "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected onRendered() {
|
|
69
|
+
// 在渲染完成后应用语法高亮(防止重复高亮)
|
|
70
|
+
if (!this.isHighlighting) {
|
|
71
|
+
this.highlightCode();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected onAttributeChanged(name: string, _oldValue: string, newValue: string) {
|
|
76
|
+
// 只在组件已连接时处理属性变化,避免在 render() 阶段触发重渲染
|
|
77
|
+
if (!this.connected) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (name) {
|
|
82
|
+
case "code":
|
|
83
|
+
this.code = newValue || "";
|
|
84
|
+
// @state 会自动触发重渲染,onRendered 会在渲染完成后调用 highlightCode()
|
|
85
|
+
break;
|
|
86
|
+
case "title":
|
|
87
|
+
this.codeTitle = newValue || "";
|
|
88
|
+
// @state 会自动触发重渲染
|
|
89
|
+
break;
|
|
90
|
+
case "language":
|
|
91
|
+
this.language = newValue || "typescript";
|
|
92
|
+
// @state 会自动触发重渲染,onRendered 会在渲染完成后调用 highlightCode()
|
|
93
|
+
break;
|
|
94
|
+
case "show-copy":
|
|
95
|
+
this.showCopy = newValue !== "false";
|
|
96
|
+
// @state 会自动触发重渲染
|
|
97
|
+
break;
|
|
98
|
+
case "show-try-online":
|
|
99
|
+
this.showTryOnline = newValue === "true";
|
|
100
|
+
// @state 会自动触发重渲染
|
|
101
|
+
break;
|
|
102
|
+
case "try-online-url":
|
|
103
|
+
this.tryOnlineUrl = newValue || "";
|
|
104
|
+
// @state 会自动触发重渲染
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 通过方法设置配置(用于编程式使用)
|
|
111
|
+
*/
|
|
112
|
+
public updateConfig(config: CodeBlockConfig): void {
|
|
113
|
+
// 批量更新所有属性,减少重渲染次数
|
|
114
|
+
// 由于 @state 使用 scheduleRerender(),多个属性更新会被批量处理
|
|
115
|
+
if (config.segments && config.segments.length > 0) {
|
|
116
|
+
this.segments = config.segments;
|
|
117
|
+
this.code = ""; // 清空单个代码块
|
|
118
|
+
} else if (config.code) {
|
|
119
|
+
this.code = config.code;
|
|
120
|
+
this.segments = []; // 清空代码段
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 清除高亮标记和元素引用,允许重新高亮
|
|
124
|
+
this.highlighted = false;
|
|
125
|
+
this.codeElements = []; // 清空元素引用,让 onRendered 重新收集
|
|
126
|
+
|
|
127
|
+
// 清除已高亮的标记(如果元素还在 DOM 中)
|
|
128
|
+
if (this.connected) {
|
|
129
|
+
this.querySelectorAll("code[data-prism-highlighted]").forEach((el) => {
|
|
130
|
+
el.removeAttribute("data-prism-highlighted");
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.codeTitle = config.title || "";
|
|
135
|
+
this.language = config.language || "typescript";
|
|
136
|
+
this.showCopy = config.showCopy !== false;
|
|
137
|
+
this.showTryOnline = config.showTryOnline === true;
|
|
138
|
+
this.tryOnlineUrl = config.tryOnlineUrl || "";
|
|
139
|
+
this.onTryOnlineCallback = config.onTryOnline;
|
|
140
|
+
// @state 会自动触发重渲染,onRendered 会在渲染完成后调用 highlightCode()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
render() {
|
|
144
|
+
// 确定要显示的代码段
|
|
145
|
+
const displaySegments =
|
|
146
|
+
this.segments.length > 0
|
|
147
|
+
? this.segments
|
|
148
|
+
: this.code
|
|
149
|
+
? [{ code: this.code, language: this.language }]
|
|
150
|
+
: [];
|
|
151
|
+
|
|
152
|
+
// 始终返回有效的 DOM 节点,不能返回 null
|
|
153
|
+
if (displaySegments.length === 0) {
|
|
154
|
+
return <div class="code-block"></div>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 合并所有代码用于复制功能
|
|
158
|
+
const allCode = displaySegments.map((seg) => seg.code).join("\n\n");
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div class="code-block">
|
|
162
|
+
{(this.codeTitle || this.showCopy || this.showTryOnline) && (
|
|
163
|
+
<div class="code-header">
|
|
164
|
+
{this.codeTitle && <span class="code-title">{this.codeTitle}</span>}
|
|
165
|
+
<div class="code-actions">
|
|
166
|
+
{this.showCopy && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => this.copyCode(allCode)}
|
|
169
|
+
class={`btn-copy ${this.copied ? "copied" : ""}`}
|
|
170
|
+
>
|
|
171
|
+
{this.copied ? "✓ Copied!" : "Copy"}
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
{this.showTryOnline && (
|
|
175
|
+
<button onClick={this.openPlayground} class="btn-try">
|
|
176
|
+
Try Online
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
<div class="code-content">
|
|
183
|
+
{displaySegments.map((segment, index) => (
|
|
184
|
+
<pre key={index} class="code-segment">
|
|
185
|
+
<code
|
|
186
|
+
class={`language-${segment.language}`}
|
|
187
|
+
ref={(element) => {
|
|
188
|
+
if (element) {
|
|
189
|
+
// 只保存引用,高亮在 onRendered() 中统一处理
|
|
190
|
+
this.codeElements[index] = element as HTMLElement;
|
|
191
|
+
}
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{segment.code}
|
|
195
|
+
</code>
|
|
196
|
+
</pre>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private copyCode = async (codeToCopy?: string): Promise<void> => {
|
|
204
|
+
const code = codeToCopy || this.code || this.segments.map((s) => s.code).join("\n\n");
|
|
205
|
+
try {
|
|
206
|
+
await navigator.clipboard.writeText(code);
|
|
207
|
+
this.copied = true;
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
this.copied = false;
|
|
210
|
+
}, 2000);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error("Failed to copy code:", error);
|
|
213
|
+
// Fallback for older browsers
|
|
214
|
+
const textArea = document.createElement("textarea");
|
|
215
|
+
textArea.value = code;
|
|
216
|
+
textArea.style.position = "fixed";
|
|
217
|
+
textArea.style.opacity = "0";
|
|
218
|
+
document.body.appendChild(textArea);
|
|
219
|
+
textArea.select();
|
|
220
|
+
try {
|
|
221
|
+
document.execCommand("copy");
|
|
222
|
+
this.copied = true;
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
this.copied = false;
|
|
225
|
+
}, 2000);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error("Fallback copy failed:", err);
|
|
228
|
+
}
|
|
229
|
+
document.body.removeChild(textArea);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
private openPlayground = (): void => {
|
|
234
|
+
if (this.onTryOnlineCallback) {
|
|
235
|
+
this.onTryOnlineCallback();
|
|
236
|
+
} else if (this.tryOnlineUrl) {
|
|
237
|
+
window.location.href = this.tryOnlineUrl;
|
|
238
|
+
} else {
|
|
239
|
+
window.location.href = "/playground";
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 应用语法高亮
|
|
245
|
+
*/
|
|
246
|
+
private highlightCode(): void {
|
|
247
|
+
// 防止重复高亮
|
|
248
|
+
if (this.isHighlighting) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.isHighlighting = true;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// 使用 requestAnimationFrame 确保 DOM 已完全更新
|
|
256
|
+
requestAnimationFrame(() => {
|
|
257
|
+
try {
|
|
258
|
+
// 如果有代码段,高亮所有代码段
|
|
259
|
+
if (this.segments.length > 0) {
|
|
260
|
+
this.segments.forEach((segment, index) => {
|
|
261
|
+
const codeEl =
|
|
262
|
+
this.codeElements[index] ||
|
|
263
|
+
(this.querySelectorAll("code")[index] as HTMLElement | null);
|
|
264
|
+
if (codeEl && !codeEl.hasAttribute("data-prism-highlighted")) {
|
|
265
|
+
const lang = segment.language.toLowerCase();
|
|
266
|
+
codeEl.className = `language-${lang}`;
|
|
267
|
+
Prism.highlightElement(codeEl);
|
|
268
|
+
codeEl.setAttribute("data-prism-highlighted", "true");
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
} else if (this.code) {
|
|
272
|
+
// 单个代码块
|
|
273
|
+
const codeEl =
|
|
274
|
+
this.codeElements[0] ||
|
|
275
|
+
(this.querySelector("code") as HTMLElement | null);
|
|
276
|
+
if (codeEl && !codeEl.hasAttribute("data-prism-highlighted")) {
|
|
277
|
+
const lang = this.language.toLowerCase();
|
|
278
|
+
codeEl.className = `language-${lang}`;
|
|
279
|
+
Prism.highlightElement(codeEl);
|
|
280
|
+
codeEl.setAttribute("data-prism-highlighted", "true");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.highlighted = true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error("Failed to highlight code:", error);
|
|
286
|
+
// 如果高亮失败,仍然显示原始代码
|
|
287
|
+
} finally {
|
|
288
|
+
this.isHighlighting = false;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("Failed to schedule highlight:", error);
|
|
293
|
+
this.isHighlighting = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
package/src/ColorPicker.wsx
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - 完全通用,不依赖特定UI库
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { WebComponent, autoRegister
|
|
12
|
+
import { WebComponent, autoRegister } from "@wsxjs/wsx-core";
|
|
13
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
13
14
|
import {
|
|
14
15
|
handleCSSVariables,
|
|
15
16
|
setDefaultColorCache,
|
|
@@ -134,7 +135,7 @@ export default class ColorPicker extends WebComponent {
|
|
|
134
135
|
*/
|
|
135
136
|
private renderColorButton(): HTMLElement {
|
|
136
137
|
return (
|
|
137
|
-
<
|
|
138
|
+
<wsx-button
|
|
138
139
|
type="button"
|
|
139
140
|
class={`color-btn ${this.disabled ? "disabled" : ""}`}
|
|
140
141
|
style={`--theme-color: ${this.selectedColor}`}
|
|
@@ -145,7 +146,7 @@ export default class ColorPicker extends WebComponent {
|
|
|
145
146
|
}}
|
|
146
147
|
>
|
|
147
148
|
<span class="color-indicator">_</span>
|
|
148
|
-
</
|
|
149
|
+
</wsx-button>
|
|
149
150
|
);
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -174,7 +175,7 @@ export default class ColorPicker extends WebComponent {
|
|
|
174
175
|
*/
|
|
175
176
|
private renderCustomPicker(): HTMLElement {
|
|
176
177
|
return (
|
|
177
|
-
<
|
|
178
|
+
<wsx-button
|
|
178
179
|
type="button"
|
|
179
180
|
class="color-cube custom-picker"
|
|
180
181
|
style={{ backgroundColor: this.customColor }}
|
|
@@ -189,7 +190,7 @@ export default class ColorPicker extends WebComponent {
|
|
|
189
190
|
*/
|
|
190
191
|
private renderColorButtons(): HTMLElement[] {
|
|
191
192
|
return this.colorCollections.map((color) => (
|
|
192
|
-
<
|
|
193
|
+
<wsx-button
|
|
193
194
|
key={color}
|
|
194
195
|
type="button"
|
|
195
196
|
class="color-cube"
|
package/src/Combobox.css
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* Combobox Component Styles */
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: inline-block;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.combobox-container {
|
|
9
|
+
position: relative;
|
|
10
|
+
display: inline-block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.combobox-input-wrapper {
|
|
15
|
+
position: relative;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 0.5rem;
|
|
19
|
+
padding: 0.5rem 0.75rem;
|
|
20
|
+
min-width: 200px;
|
|
21
|
+
background: var(--combobox-bg, rgba(255, 255, 255, 0.1));
|
|
22
|
+
border: 1px solid var(--combobox-border, rgba(255, 255, 255, 0.2));
|
|
23
|
+
border-radius: var(--combobox-border-radius, 0.5rem);
|
|
24
|
+
transition: all 0.2s ease;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.combobox-input-wrapper:focus-within {
|
|
28
|
+
background: var(--combobox-focus-bg, rgba(255, 255, 255, 0.15));
|
|
29
|
+
border-color: var(--combobox-focus-border, var(--focus-color, #dc2626));
|
|
30
|
+
outline: 2px solid var(--combobox-focus-outline, rgba(220, 38, 38, 0.2));
|
|
31
|
+
outline-offset: 2px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.combobox-tags {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.25rem;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
flex: 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.combobox-tag {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 0.25rem;
|
|
46
|
+
padding: 0.25rem 0.5rem;
|
|
47
|
+
background: var(--combobox-tag-bg, #dc2626);
|
|
48
|
+
color: var(--combobox-tag-color, #ffffff);
|
|
49
|
+
border-radius: 0.25rem;
|
|
50
|
+
font-size: 0.75rem;
|
|
51
|
+
font-weight: 500;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.combobox-tag-remove {
|
|
55
|
+
background: none;
|
|
56
|
+
border: none;
|
|
57
|
+
color: var(--combobox-tag-color, #ffffff);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
font-size: 1rem;
|
|
60
|
+
line-height: 1;
|
|
61
|
+
padding: 0;
|
|
62
|
+
width: 16px;
|
|
63
|
+
height: 16px;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
transition: background-color 0.15s ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.combobox-tag-remove:hover {
|
|
72
|
+
background: rgba(255, 255, 255, 0.2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.combobox-tag-more {
|
|
76
|
+
padding: 0.25rem 0.5rem;
|
|
77
|
+
color: var(--combobox-tag-color, #ffffff);
|
|
78
|
+
font-size: 0.75rem;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.combobox-input {
|
|
83
|
+
flex: 1;
|
|
84
|
+
border: none;
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: var(--combobox-color, currentColor);
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
outline: none;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.combobox-input::placeholder {
|
|
94
|
+
color: var(--combobox-placeholder-color, rgba(255, 255, 255, 0.5));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.combobox-input:disabled {
|
|
98
|
+
opacity: 0.5;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.combobox-arrow {
|
|
103
|
+
background: none;
|
|
104
|
+
border: none;
|
|
105
|
+
color: var(--combobox-arrow-color, currentColor);
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
font-size: 0.625rem;
|
|
108
|
+
opacity: 0.7;
|
|
109
|
+
transition: transform 0.2s ease;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
padding: 0.25rem;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.combobox-arrow:hover {
|
|
118
|
+
opacity: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.combobox-arrow.open {
|
|
122
|
+
transform: rotate(180deg);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.combobox-arrow:disabled {
|
|
126
|
+
opacity: 0.5;
|
|
127
|
+
cursor: not-allowed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.combobox-menu {
|
|
131
|
+
position: absolute;
|
|
132
|
+
z-index: 1000;
|
|
133
|
+
min-width: 100%;
|
|
134
|
+
max-width: 300px;
|
|
135
|
+
max-height: 300px;
|
|
136
|
+
overflow-y: auto;
|
|
137
|
+
background: var(--combobox-menu-bg, #ffffff);
|
|
138
|
+
border: 1px solid var(--combobox-menu-border, #e5e7eb);
|
|
139
|
+
border-radius: var(--combobox-border-radius, 0.5rem);
|
|
140
|
+
box-shadow:
|
|
141
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
142
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
143
|
+
padding: 0.25rem;
|
|
144
|
+
margin: 0.25rem 0 0 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[data-theme="dark"] .combobox-menu {
|
|
148
|
+
background: var(--combobox-menu-bg-dark, #1f2937);
|
|
149
|
+
border-color: var(--combobox-menu-border-dark, #374151);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.combobox-menu.combobox-top {
|
|
153
|
+
bottom: 100%;
|
|
154
|
+
margin: 0 0 0.25rem 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.combobox-menu.combobox-bottom {
|
|
158
|
+
top: 100%;
|
|
159
|
+
margin: 0.25rem 0 0 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.combobox-menu.combobox-left {
|
|
163
|
+
left: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.combobox-menu.combobox-right {
|
|
167
|
+
right: 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.combobox-menu.combobox-center {
|
|
171
|
+
left: 50%;
|
|
172
|
+
transform: translateX(-50%);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.combobox-menu.combobox-top.combobox-center {
|
|
176
|
+
transform: translateX(-50%) translateY(-100%);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.combobox-option {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 0.5rem;
|
|
183
|
+
width: 100%;
|
|
184
|
+
padding: 0.5rem 0.75rem;
|
|
185
|
+
background: transparent;
|
|
186
|
+
border: none;
|
|
187
|
+
border-radius: 0.375rem;
|
|
188
|
+
color: var(--combobox-option-color, #1f2937);
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
font-size: 0.875rem;
|
|
191
|
+
text-align: left;
|
|
192
|
+
transition: background-color 0.15s ease;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[data-theme="dark"] .combobox-option {
|
|
196
|
+
color: var(--combobox-option-color-dark, #f3f4f6);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.combobox-option:hover:not(.disabled) {
|
|
200
|
+
background: var(--combobox-option-hover-bg, #f3f4f6);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[data-theme="dark"] .combobox-option:hover:not(.disabled) {
|
|
204
|
+
background: var(--combobox-option-hover-bg-dark, #374151);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.combobox-option.selected {
|
|
208
|
+
background: var(--combobox-option-selected-bg, #dc2626);
|
|
209
|
+
color: var(--combobox-option-selected-color, #ffffff);
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.combobox-option.selected:hover {
|
|
214
|
+
background: var(--combobox-option-selected-hover-bg, #b91c1c);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.combobox-option.disabled {
|
|
218
|
+
opacity: 0.5;
|
|
219
|
+
cursor: not-allowed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.combobox-checkbox {
|
|
223
|
+
width: 16px;
|
|
224
|
+
height: 16px;
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
font-size: 0.75rem;
|
|
230
|
+
font-weight: bold;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.combobox-empty {
|
|
234
|
+
padding: 0.5rem 0.75rem;
|
|
235
|
+
color: var(--combobox-empty-color, #9ca3af);
|
|
236
|
+
font-size: 0.875rem;
|
|
237
|
+
text-align: center;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Responsive design */
|
|
241
|
+
@media (max-width: 768px) {
|
|
242
|
+
.combobox-input-wrapper {
|
|
243
|
+
min-width: 150px;
|
|
244
|
+
padding: 0.375rem 0.5rem;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.combobox-input {
|
|
248
|
+
font-size: 0.8125rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.combobox-menu {
|
|
252
|
+
max-width: 200px;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox Types
|
|
3
|
+
* 组合框组件的类型定义
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ComboboxOption {
|
|
7
|
+
/** 选项值 */
|
|
8
|
+
value: string;
|
|
9
|
+
/** 选项标签 */
|
|
10
|
+
label: string;
|
|
11
|
+
/** 是否禁用 */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** 自定义渲染函数 */
|
|
14
|
+
render?: () => HTMLElement;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ComboboxConfig {
|
|
18
|
+
/** 占位符文本 */
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** 是否禁用 */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** 是否支持多选 */
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
/** 是否可搜索 */
|
|
25
|
+
searchable?: boolean;
|
|
26
|
+
/** 下拉菜单位置(top/bottom) */
|
|
27
|
+
position?: "top" | "bottom";
|
|
28
|
+
/** 下拉菜单对齐方式(left/right/center) */
|
|
29
|
+
align?: "left" | "right" | "center";
|
|
30
|
+
/** 触发方式(click/hover) */
|
|
31
|
+
trigger?: "click" | "hover";
|
|
32
|
+
}
|