@wsxjs/wsx-base-components 0.0.5
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 +21 -0
- package/README.md +204 -0
- package/dist/index.cjs +6 -0
- package/dist/index.js +1085 -0
- package/package.json +57 -0
- package/src/ColorPicker.css +188 -0
- package/src/ColorPicker.wsx +416 -0
- package/src/ColorPickerUtils.ts +116 -0
- package/src/ReactiveCounter.css +304 -0
- package/src/ReactiveCounter.wsx +244 -0
- package/src/SimpleReactiveDemo.wsx +58 -0
- package/src/SvgDemo.wsx +241 -0
- package/src/SvgIcon.wsx +88 -0
- package/src/ThemeSwitcher.css +91 -0
- package/src/ThemeSwitcher.wsx +97 -0
- package/src/XyButton.css +257 -0
- package/src/XyButton.wsx +356 -0
- package/src/XyButtonGroup.css +30 -0
- package/src/XyButtonGroup.wsx +124 -0
- package/src/index.ts +17 -0
- package/src/jsx-inject.ts +2 -0
- package/src/types/wsx.d.ts +6 -0
package/src/SvgDemo.wsx
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
import { WebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
3
|
+
|
|
4
|
+
const logger = createLogger("SvgDemo");
|
|
5
|
+
|
|
6
|
+
@autoRegister({ tagName: "svg-demo" })
|
|
7
|
+
export default class SvgDemo extends WebComponent {
|
|
8
|
+
private animationId: number | null = null;
|
|
9
|
+
private rotationAngle = 0;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
logger.info("SvgDemo component initialized");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
render() {
|
|
17
|
+
return (
|
|
18
|
+
<div style="padding: 20px; background: #f5f5f5; border-radius: 8px; margin: 10px 0;">
|
|
19
|
+
<h3 style="margin-top: 0; color: #333;">SVG Showcase</h3>
|
|
20
|
+
|
|
21
|
+
{/* Basic SVG shapes */}
|
|
22
|
+
<div style="margin-bottom: 20px;">
|
|
23
|
+
<h4 style="margin: 10px 0; color: #666;">Basic Shapes</h4>
|
|
24
|
+
<svg
|
|
25
|
+
width="300"
|
|
26
|
+
height="100"
|
|
27
|
+
style="border: 1px solid #ddd; background: white;"
|
|
28
|
+
>
|
|
29
|
+
<circle
|
|
30
|
+
cx="50"
|
|
31
|
+
cy="50"
|
|
32
|
+
r="30"
|
|
33
|
+
fill="#e74c3c"
|
|
34
|
+
stroke="#c0392b"
|
|
35
|
+
strokeWidth="2"
|
|
36
|
+
/>
|
|
37
|
+
<rect
|
|
38
|
+
x="100"
|
|
39
|
+
y="20"
|
|
40
|
+
width="60"
|
|
41
|
+
height="60"
|
|
42
|
+
fill="#3498db"
|
|
43
|
+
stroke="#2980b9"
|
|
44
|
+
strokeWidth="2"
|
|
45
|
+
rx="5"
|
|
46
|
+
/>
|
|
47
|
+
<polygon
|
|
48
|
+
points="200,20 230,80 170,80"
|
|
49
|
+
fill="#2ecc71"
|
|
50
|
+
stroke="#27ae60"
|
|
51
|
+
strokeWidth="2"
|
|
52
|
+
/>
|
|
53
|
+
<line
|
|
54
|
+
x1="250"
|
|
55
|
+
y1="20"
|
|
56
|
+
x2="290"
|
|
57
|
+
y2="80"
|
|
58
|
+
stroke="#9b59b6"
|
|
59
|
+
strokeWidth="3"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* SVG with gradients */}
|
|
66
|
+
<div style="margin-bottom: 20px;">
|
|
67
|
+
<h4 style="margin: 10px 0; color: #666;">Gradients & Effects</h4>
|
|
68
|
+
<svg
|
|
69
|
+
width="300"
|
|
70
|
+
height="100"
|
|
71
|
+
style="border: 1px solid #ddd; background: white;"
|
|
72
|
+
>
|
|
73
|
+
<defs>
|
|
74
|
+
<linearGradient id="blueGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
75
|
+
<stop offset="0%" stopColor="#3498db" />
|
|
76
|
+
<stop offset="100%" stopColor="#9b59b6" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<radialGradient id="redGradient" cx="50%" cy="50%" r="50%">
|
|
79
|
+
<stop offset="0%" stopColor="#e74c3c" />
|
|
80
|
+
<stop offset="100%" stopColor="#c0392b" />
|
|
81
|
+
</radialGradient>
|
|
82
|
+
</defs>
|
|
83
|
+
<rect
|
|
84
|
+
x="20"
|
|
85
|
+
y="20"
|
|
86
|
+
width="120"
|
|
87
|
+
height="60"
|
|
88
|
+
fill="url(#blueGradient)"
|
|
89
|
+
rx="10"
|
|
90
|
+
/>
|
|
91
|
+
<circle cx="200" cy="50" r="35" fill="url(#redGradient)" />
|
|
92
|
+
</svg>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Animated SVG */}
|
|
96
|
+
<div style="margin-bottom: 20px;">
|
|
97
|
+
<h4 style="margin: 10px 0; color: #666;">Animation</h4>
|
|
98
|
+
<svg
|
|
99
|
+
width="300"
|
|
100
|
+
height="100"
|
|
101
|
+
style="border: 1px solid #ddd; background: white;"
|
|
102
|
+
>
|
|
103
|
+
<g transform={`translate(150, 50) rotate(${this.rotationAngle})`}>
|
|
104
|
+
<polygon
|
|
105
|
+
points="-30,0 0,-40 30,0 0,40"
|
|
106
|
+
fill="#f39c12"
|
|
107
|
+
stroke="#e67e22"
|
|
108
|
+
strokeWidth="2"
|
|
109
|
+
/>
|
|
110
|
+
<circle cx="0" cy="0" r="8" fill="#2c3e50" />
|
|
111
|
+
</g>
|
|
112
|
+
</svg>
|
|
113
|
+
<div style="margin-top: 10px;">
|
|
114
|
+
<button
|
|
115
|
+
onClick={this.startAnimation}
|
|
116
|
+
style="margin-right: 10px; padding: 5px 10px;"
|
|
117
|
+
>
|
|
118
|
+
Start Animation
|
|
119
|
+
</button>
|
|
120
|
+
<button onClick={this.stopAnimation} style="padding: 5px 10px;">
|
|
121
|
+
Stop Animation
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Interactive SVG chart */}
|
|
127
|
+
<div style="margin-bottom: 20px;">
|
|
128
|
+
<h4 style="margin: 10px 0; color: #666;">Interactive Chart</h4>
|
|
129
|
+
{this.renderChart()}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* SVG Icons */}
|
|
133
|
+
<div>
|
|
134
|
+
<h4 style="margin: 10px 0; color: #666;">Icon Components</h4>
|
|
135
|
+
<div style="display: flex; gap: 15px; align-items: center;">
|
|
136
|
+
<svg-icon name="star" size="32" color="#f39c12"></svg-icon>
|
|
137
|
+
<svg-icon name="heart" size="32" color="#e74c3c"></svg-icon>
|
|
138
|
+
<svg-icon name="check" size="32" color="#27ae60"></svg-icon>
|
|
139
|
+
<svg-icon name="github" size="32" color="#333"></svg-icon>
|
|
140
|
+
<svg-icon name="play" size="32" color="#3498db"></svg-icon>
|
|
141
|
+
<svg-icon name="settings" size="32" color="#9b59b6"></svg-icon>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private renderChart() {
|
|
149
|
+
const data = [30, 80, 45, 60, 20, 90, 55];
|
|
150
|
+
const maxValue = Math.max(...data);
|
|
151
|
+
const barWidth = 30;
|
|
152
|
+
const barSpacing = 40;
|
|
153
|
+
const chartHeight = 120;
|
|
154
|
+
const chartWidth = data.length * barSpacing + 40;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<svg
|
|
158
|
+
width={chartWidth}
|
|
159
|
+
height={chartHeight + 40}
|
|
160
|
+
style="border: 1px solid #ddd; background: white;"
|
|
161
|
+
>
|
|
162
|
+
{/* Chart bars */}
|
|
163
|
+
{data.map((value, index) => {
|
|
164
|
+
const barHeight = (value / maxValue) * chartHeight;
|
|
165
|
+
const x = index * barSpacing + 20;
|
|
166
|
+
const y = chartHeight - barHeight + 20;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<g key={index}>
|
|
170
|
+
<rect
|
|
171
|
+
x={x}
|
|
172
|
+
y={y}
|
|
173
|
+
width={barWidth}
|
|
174
|
+
height={barHeight}
|
|
175
|
+
fill="#3498db"
|
|
176
|
+
stroke="#2980b9"
|
|
177
|
+
strokeWidth="1"
|
|
178
|
+
onMouseEnter={(e) => this.showTooltip(e, value)}
|
|
179
|
+
onMouseLeave={this.hideTooltip}
|
|
180
|
+
style="cursor: pointer; transition: fill 0.2s;"
|
|
181
|
+
/>
|
|
182
|
+
<text
|
|
183
|
+
x={x + barWidth / 2}
|
|
184
|
+
y={chartHeight + 35}
|
|
185
|
+
textAnchor="middle"
|
|
186
|
+
fontSize="12"
|
|
187
|
+
fill="#666"
|
|
188
|
+
>
|
|
189
|
+
{index + 1}
|
|
190
|
+
</text>
|
|
191
|
+
</g>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</svg>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private showTooltip = (event: Event, value: number) => {
|
|
199
|
+
const rect = event.target as SVGRectElement;
|
|
200
|
+
rect.setAttribute("fill", "#e74c3c");
|
|
201
|
+
|
|
202
|
+
// You could create a proper tooltip here
|
|
203
|
+
logger.debug(`Tooltip value: ${value}`);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
private hideTooltip = (event: Event) => {
|
|
207
|
+
const rect = event.target as SVGRectElement;
|
|
208
|
+
rect.setAttribute("fill", "#3498db");
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
private startAnimation = () => {
|
|
212
|
+
if (this.animationId) return;
|
|
213
|
+
|
|
214
|
+
const animate = () => {
|
|
215
|
+
this.rotationAngle += 2;
|
|
216
|
+
if (this.rotationAngle >= 360) {
|
|
217
|
+
this.rotationAngle = 0;
|
|
218
|
+
}
|
|
219
|
+
this.rerender();
|
|
220
|
+
this.animationId = requestAnimationFrame(animate);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
this.animationId = requestAnimationFrame(animate);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
private stopAnimation = () => {
|
|
227
|
+
if (this.animationId) {
|
|
228
|
+
cancelAnimationFrame(this.animationId);
|
|
229
|
+
this.animationId = null;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
protected onConnected(): void {
|
|
234
|
+
logger.info("SvgDemo connected to DOM");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected onDisconnected(): void {
|
|
238
|
+
logger.info("SvgDemo disconnected from DOM");
|
|
239
|
+
this.stopAnimation();
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/SvgIcon.wsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
import { WebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
3
|
+
|
|
4
|
+
const logger = createLogger("SvgIcon");
|
|
5
|
+
|
|
6
|
+
@autoRegister({ tagName: "svg-icon" })
|
|
7
|
+
export default class SvgIcon extends WebComponent {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
logger.info("SvgIcon component initialized");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
render() {
|
|
14
|
+
const size = this.getAttribute("size") || "24";
|
|
15
|
+
const color = this.getAttribute("color") || "currentColor";
|
|
16
|
+
const name = this.getAttribute("name") || "star";
|
|
17
|
+
|
|
18
|
+
// Different icon paths based on name
|
|
19
|
+
const iconPaths = {
|
|
20
|
+
star: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
|
|
21
|
+
heart: "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z",
|
|
22
|
+
check: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z",
|
|
23
|
+
close: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
|
24
|
+
github: "M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22",
|
|
25
|
+
play: "M8 5v14l11-7z",
|
|
26
|
+
settings:
|
|
27
|
+
"M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const path = iconPaths[name as keyof typeof iconPaths] || iconPaths.star;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
width={size}
|
|
35
|
+
height={size}
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke={color}
|
|
39
|
+
strokeWidth="2"
|
|
40
|
+
strokeLinecap="round"
|
|
41
|
+
strokeLinejoin="round"
|
|
42
|
+
className="svg-icon"
|
|
43
|
+
onClick={this.handleClick}
|
|
44
|
+
style="cursor: pointer; transition: transform 0.2s ease; display: inline-block;"
|
|
45
|
+
onMouseEnter={this.handleMouseEnter}
|
|
46
|
+
onMouseLeave={this.handleMouseLeave}
|
|
47
|
+
>
|
|
48
|
+
<path d={path} fill={color} />
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private handleClick = (event: Event) => {
|
|
54
|
+
logger.debug("SVG icon clicked", { name: this.getAttribute("name") });
|
|
55
|
+
|
|
56
|
+
// Dispatch custom event
|
|
57
|
+
this.dispatchEvent(
|
|
58
|
+
new CustomEvent("icon-click", {
|
|
59
|
+
detail: {
|
|
60
|
+
name: this.getAttribute("name"),
|
|
61
|
+
originalEvent: event,
|
|
62
|
+
},
|
|
63
|
+
bubbles: true,
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
private handleMouseEnter = (event: Event) => {
|
|
69
|
+
const svg = event.target as SVGElement;
|
|
70
|
+
svg.style.transform = "scale(1.1)";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
private handleMouseLeave = (event: Event) => {
|
|
74
|
+
const svg = event.target as SVGElement;
|
|
75
|
+
svg.style.transform = "scale(1)";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
static get observedAttributes() {
|
|
79
|
+
return ["name", "size", "color"];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected onAttributeChanged(name: string, oldValue: string, newValue: string): void {
|
|
83
|
+
logger.debug(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
|
|
84
|
+
if (this.connected) {
|
|
85
|
+
this.rerender();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* ThemeSwitcher Component Styles */
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.theme-switcher-container {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.theme-switcher-btn {
|
|
14
|
+
width: var(--theme-switcher-width, 2.5rem);
|
|
15
|
+
height: var(--theme-switcher-height, 2.5rem);
|
|
16
|
+
padding: var(--theme-switcher-padding, 0.5rem);
|
|
17
|
+
border-radius: var(--theme-switcher-border-radius, 8px);
|
|
18
|
+
background: var(--theme-switcher-bg, #dc2626);
|
|
19
|
+
border: var(--theme-switcher-border, none);
|
|
20
|
+
color: var(--theme-switcher-color, white);
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
transition: all var(--theme-switcher-transition, 0.3s ease);
|
|
26
|
+
box-shadow: var(--theme-switcher-shadow, 0 4px 15px rgba(220, 38, 38, 0.4));
|
|
27
|
+
font-weight: var(--theme-switcher-font-weight, 600);
|
|
28
|
+
font-family: var(--theme-switcher-font-family, inherit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.theme-switcher-btn:hover {
|
|
32
|
+
background: var(--theme-switcher-hover-bg, #b91c1c);
|
|
33
|
+
transform: var(--theme-switcher-hover-transform, translateY(-2px));
|
|
34
|
+
box-shadow: var(--theme-switcher-hover-shadow, 0 8px 25px rgba(220, 38, 38, 0.5));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.theme-switcher-btn:active {
|
|
38
|
+
transform: var(--theme-switcher-active-transform, translateY(0));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.theme-switcher-icon {
|
|
42
|
+
font-size: var(--theme-switcher-icon-size, 1rem);
|
|
43
|
+
line-height: 1;
|
|
44
|
+
transition: transform var(--theme-switcher-icon-transition, 0.3s ease);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.theme-switcher-btn:hover .theme-switcher-icon {
|
|
48
|
+
transform: var(--theme-switcher-icon-hover-transform, rotate(360deg));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Light theme specific styles */
|
|
52
|
+
.theme-switcher-btn[data-theme="light"] {
|
|
53
|
+
background: var(--theme-switcher-light-bg, #dc2626);
|
|
54
|
+
color: var(--theme-switcher-light-color, white);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.theme-switcher-btn[data-theme="light"]:hover {
|
|
58
|
+
background: var(--theme-switcher-light-hover-bg, #b91c1c);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Dark theme specific styles */
|
|
62
|
+
.theme-switcher-btn[data-theme="dark"] {
|
|
63
|
+
background: var(--theme-switcher-dark-bg, #dc2626);
|
|
64
|
+
color: var(--theme-switcher-dark-color, white);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.theme-switcher-btn[data-theme="dark"]:hover {
|
|
68
|
+
background: var(--theme-switcher-dark-hover-bg, #b91c1c);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Auto theme specific styles */
|
|
72
|
+
.theme-switcher-btn[data-theme="auto"] {
|
|
73
|
+
background: var(--theme-switcher-auto-bg, linear-gradient(135deg, #dc2626, #b91c1c));
|
|
74
|
+
color: var(--theme-switcher-auto-color, white);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.theme-switcher-btn[data-theme="auto"]:hover {
|
|
78
|
+
background: var(--theme-switcher-auto-hover-bg, linear-gradient(135deg, #b91c1c, #991b1b));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Responsive design */
|
|
82
|
+
@media (max-width: 768px) {
|
|
83
|
+
.theme-switcher-btn {
|
|
84
|
+
width: var(--theme-switcher-mobile-width, 2rem);
|
|
85
|
+
height: var(--theme-switcher-mobile-height, 2rem);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.theme-switcher-icon {
|
|
89
|
+
font-size: var(--theme-switcher-mobile-icon-size, 0.9rem);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
|
|
3
|
+
import { WebComponent, autoRegister } from "@wsxjs/wsx-core";
|
|
4
|
+
import styles from "./ThemeSwitcher.css?inline";
|
|
5
|
+
|
|
6
|
+
@autoRegister({ tagName: "theme-switcher" })
|
|
7
|
+
export default class ThemeSwitcher extends WebComponent {
|
|
8
|
+
private currentTheme: "light" | "dark" | "auto" = "auto";
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super({
|
|
12
|
+
styles,
|
|
13
|
+
styleName: "theme-switcher",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.initTheme();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render(): HTMLElement {
|
|
20
|
+
return (
|
|
21
|
+
<div class="theme-switcher-container">
|
|
22
|
+
<button
|
|
23
|
+
class="theme-switcher-btn"
|
|
24
|
+
data-theme={this.currentTheme}
|
|
25
|
+
onClick={this.toggleTheme}
|
|
26
|
+
title={`当前主题: ${this.getThemeLabel()}`}
|
|
27
|
+
>
|
|
28
|
+
<span class="theme-switcher-icon">{this.getThemeIcon()}</span>
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private getThemeIcon(): string {
|
|
35
|
+
// 根据当前实际应用的主题显示图标,而不是设置的主题
|
|
36
|
+
const html = document.documentElement;
|
|
37
|
+
const isDark = html.classList.contains("dark");
|
|
38
|
+
|
|
39
|
+
if (this.currentTheme === "auto") {
|
|
40
|
+
// 自动模式:根据系统主题显示对应图标
|
|
41
|
+
return isDark ? "🌙" : "☀️";
|
|
42
|
+
} else if (this.currentTheme === "light") {
|
|
43
|
+
return "☀️";
|
|
44
|
+
} else {
|
|
45
|
+
return "🌙";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private getThemeLabel(): string {
|
|
50
|
+
const labels = {
|
|
51
|
+
light: "浅色",
|
|
52
|
+
dark: "深色",
|
|
53
|
+
auto: "自动",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return labels[this.currentTheme];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private toggleTheme = (): void => {
|
|
60
|
+
const themes: ("light" | "dark" | "auto")[] = ["auto", "light", "dark"];
|
|
61
|
+
const currentIndex = themes.indexOf(this.currentTheme);
|
|
62
|
+
const nextIndex = (currentIndex + 1) % themes.length;
|
|
63
|
+
this.setTheme(themes[nextIndex]);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
private setTheme(theme: "light" | "dark" | "auto"): void {
|
|
67
|
+
this.currentTheme = theme;
|
|
68
|
+
const html = document.documentElement;
|
|
69
|
+
|
|
70
|
+
if (theme === "auto") {
|
|
71
|
+
html.removeAttribute("class");
|
|
72
|
+
this.checkSystemTheme();
|
|
73
|
+
} else {
|
|
74
|
+
html.className = theme;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
localStorage.setItem("wsx-theme", theme);
|
|
78
|
+
this.rerender();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private checkSystemTheme(): void {
|
|
82
|
+
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
83
|
+
document.documentElement.className = isDark ? "dark" : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private initTheme(): void {
|
|
87
|
+
const savedTheme =
|
|
88
|
+
(localStorage.getItem("wsx-theme") as "light" | "dark" | "auto") || "auto";
|
|
89
|
+
this.setTheme(savedTheme);
|
|
90
|
+
|
|
91
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
92
|
+
if (this.currentTheme === "auto") {
|
|
93
|
+
document.documentElement.className = e.matches ? "dark" : "";
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|