@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.
- 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
package/src/SvgDemo.wsx
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
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/TodoList.css
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
.todo-list {
|
|
2
|
-
padding: 1.5rem;
|
|
3
|
-
max-width: 600px;
|
|
4
|
-
margin: 0 auto;
|
|
5
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.todo-header {
|
|
9
|
-
margin-bottom: 1.5rem;
|
|
10
|
-
text-align: center;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.todo-header h2 {
|
|
14
|
-
margin: 0 0 0.5rem 0;
|
|
15
|
-
color: #333;
|
|
16
|
-
font-size: 1.75rem;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.subtitle {
|
|
20
|
-
margin: 0;
|
|
21
|
-
color: #666;
|
|
22
|
-
font-size: 0.9rem;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.todo-input-section {
|
|
26
|
-
display: flex;
|
|
27
|
-
gap: 0.5rem;
|
|
28
|
-
margin-bottom: 1rem;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.todo-input {
|
|
32
|
-
flex: 1;
|
|
33
|
-
padding: 0.75rem;
|
|
34
|
-
border: 2px solid #ddd;
|
|
35
|
-
border-radius: 4px;
|
|
36
|
-
font-size: 1rem;
|
|
37
|
-
transition: border-color 0.2s;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.todo-input:focus {
|
|
41
|
-
outline: none;
|
|
42
|
-
border-color: #4caf50;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.todo-filters {
|
|
46
|
-
display: flex;
|
|
47
|
-
gap: 0.5rem;
|
|
48
|
-
margin-bottom: 1rem;
|
|
49
|
-
justify-content: center;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.filter-btn {
|
|
53
|
-
padding: 0.5rem 1rem;
|
|
54
|
-
border: 2px solid #ddd;
|
|
55
|
-
background: white;
|
|
56
|
-
border-radius: 4px;
|
|
57
|
-
cursor: pointer;
|
|
58
|
-
transition: all 0.2s;
|
|
59
|
-
font-size: 0.9rem;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.filter-btn:hover {
|
|
63
|
-
background: #f5f5f5;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.filter-btn.active {
|
|
67
|
-
background: #4caf50;
|
|
68
|
-
color: white;
|
|
69
|
-
border-color: #4caf50;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.todo-list-container {
|
|
73
|
-
min-height: 200px;
|
|
74
|
-
margin-bottom: 1rem;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.empty-state {
|
|
78
|
-
text-align: center;
|
|
79
|
-
padding: 3rem 1rem;
|
|
80
|
-
color: #999;
|
|
81
|
-
font-style: italic;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.todo-items {
|
|
85
|
-
list-style: none;
|
|
86
|
-
padding: 0;
|
|
87
|
-
margin: 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.todo-item {
|
|
91
|
-
display: flex;
|
|
92
|
-
align-items: center;
|
|
93
|
-
gap: 0.75rem;
|
|
94
|
-
padding: 0.75rem;
|
|
95
|
-
margin-bottom: 0.5rem;
|
|
96
|
-
background: #f9f9f9;
|
|
97
|
-
border-radius: 4px;
|
|
98
|
-
transition: background 0.2s;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.todo-item:hover {
|
|
102
|
-
background: #f0f0f0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.todo-item.completed {
|
|
106
|
-
opacity: 0.6;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.todo-checkbox {
|
|
110
|
-
width: 1.25rem;
|
|
111
|
-
height: 1.25rem;
|
|
112
|
-
cursor: pointer;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.todo-text {
|
|
116
|
-
flex: 1;
|
|
117
|
-
font-size: 1rem;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.todo-item.completed .todo-text {
|
|
121
|
-
text-decoration: line-through;
|
|
122
|
-
color: #999;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.todo-actions {
|
|
126
|
-
display: flex;
|
|
127
|
-
gap: 0.5rem;
|
|
128
|
-
justify-content: center;
|
|
129
|
-
margin-top: 1rem;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.btn {
|
|
133
|
-
padding: 0.5rem 1rem;
|
|
134
|
-
border: none;
|
|
135
|
-
border-radius: 4px;
|
|
136
|
-
cursor: pointer;
|
|
137
|
-
font-size: 0.9rem;
|
|
138
|
-
transition: all 0.2s;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.btn:disabled {
|
|
142
|
-
opacity: 0.5;
|
|
143
|
-
cursor: not-allowed;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.btn-primary {
|
|
147
|
-
background: #4caf50;
|
|
148
|
-
color: white;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.btn-primary:hover:not(:disabled) {
|
|
152
|
-
background: #45a049;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.btn-warning {
|
|
156
|
-
background: #ff9800;
|
|
157
|
-
color: white;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.btn-warning:hover {
|
|
161
|
-
background: #e68900;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.btn-danger {
|
|
165
|
-
background: #f44336;
|
|
166
|
-
color: white;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.btn-danger:hover {
|
|
170
|
-
background: #da190b;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.btn-sm {
|
|
174
|
-
padding: 0.25rem 0.5rem;
|
|
175
|
-
font-size: 0.8rem;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.debug-info {
|
|
179
|
-
margin-top: 2rem;
|
|
180
|
-
padding-top: 1rem;
|
|
181
|
-
border-top: 1px solid #ddd;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.debug-info summary {
|
|
185
|
-
cursor: pointer;
|
|
186
|
-
color: #666;
|
|
187
|
-
font-size: 0.9rem;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.debug-info pre {
|
|
191
|
-
background: #f5f5f5;
|
|
192
|
-
padding: 1rem;
|
|
193
|
-
border-radius: 4px;
|
|
194
|
-
overflow-x: auto;
|
|
195
|
-
font-size: 0.85rem;
|
|
196
|
-
margin-top: 0.5rem;
|
|
197
|
-
}
|
package/src/TodoList.wsx
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
-
/**
|
|
3
|
-
* TodoList Component - WebComponent with @state array support
|
|
4
|
-
*
|
|
5
|
-
* Demonstrates:
|
|
6
|
-
* - Using @state decorator with arrays in WebComponent
|
|
7
|
-
* - Array operations: add, remove, update, toggle
|
|
8
|
-
* - Reactive array updates trigger automatic rerender
|
|
9
|
-
* - Shadow DOM styling
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { WebComponent, state, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
13
|
-
|
|
14
|
-
const logger = createLogger("TodoList");
|
|
15
|
-
|
|
16
|
-
interface TodoItem {
|
|
17
|
-
id: number;
|
|
18
|
-
text: string;
|
|
19
|
-
completed: boolean;
|
|
20
|
-
createdAt: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@autoRegister({ tagName: "todo-list" })
|
|
24
|
-
export default class TodoList extends WebComponent {
|
|
25
|
-
// @state decorator with array - automatically reactive
|
|
26
|
-
@state private todos: TodoItem[] = [];
|
|
27
|
-
|
|
28
|
-
// Non-reactive input value - prevents rerender on every keystroke
|
|
29
|
-
private _newTodoText = "";
|
|
30
|
-
|
|
31
|
-
// @state decorator with filter state
|
|
32
|
-
@state private filter: "all" | "active" | "completed" = "all";
|
|
33
|
-
|
|
34
|
-
private nextId = 1;
|
|
35
|
-
|
|
36
|
-
constructor() {
|
|
37
|
-
super();
|
|
38
|
-
logger.info("TodoList initialized");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
render() {
|
|
42
|
-
const filteredTodos = this.getFilteredTodos();
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div class="todo-list">
|
|
46
|
-
<div class="todo-header">
|
|
47
|
-
<h2>📝 Todo List (WebComponent)</h2>
|
|
48
|
-
<p class="subtitle">
|
|
49
|
-
{this.todos.length} total, {this.getActiveCount()} active,{" "}
|
|
50
|
-
{this.getCompletedCount()} completed
|
|
51
|
-
</p>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div class="todo-input-section">
|
|
55
|
-
<input
|
|
56
|
-
type="text"
|
|
57
|
-
class="todo-input"
|
|
58
|
-
placeholder="Add a new todo..."
|
|
59
|
-
value={this._newTodoText}
|
|
60
|
-
onInput={this.handleInputChange}
|
|
61
|
-
onKeyDown={this.handleKeyDown}
|
|
62
|
-
/>
|
|
63
|
-
<button
|
|
64
|
-
class="btn btn-primary"
|
|
65
|
-
onClick={this.addTodo}
|
|
66
|
-
disabled={!this._newTodoText.trim()}
|
|
67
|
-
>
|
|
68
|
-
Add
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<div class="todo-filters">
|
|
73
|
-
<button
|
|
74
|
-
class={`filter-btn ${this.filter === "all" ? "active" : ""}`}
|
|
75
|
-
onClick={() => (this.filter = "all")}
|
|
76
|
-
>
|
|
77
|
-
All ({this.todos.length})
|
|
78
|
-
</button>
|
|
79
|
-
<button
|
|
80
|
-
class={`filter-btn ${this.filter === "active" ? "active" : ""}`}
|
|
81
|
-
onClick={() => (this.filter = "active")}
|
|
82
|
-
>
|
|
83
|
-
Active ({this.getActiveCount()})
|
|
84
|
-
</button>
|
|
85
|
-
<button
|
|
86
|
-
class={`filter-btn ${this.filter === "completed" ? "active" : ""}`}
|
|
87
|
-
onClick={() => (this.filter = "completed")}
|
|
88
|
-
>
|
|
89
|
-
Completed ({this.getCompletedCount()})
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div class="todo-list-container">
|
|
94
|
-
{filteredTodos.length === 0 ? (
|
|
95
|
-
<div class="empty-state">
|
|
96
|
-
{this.filter === "all"
|
|
97
|
-
? "No todos yet. Add one above! 🎉"
|
|
98
|
-
: `No ${this.filter} todos.`}
|
|
99
|
-
</div>
|
|
100
|
-
) : (
|
|
101
|
-
<ul class="todo-items">
|
|
102
|
-
{filteredTodos.map((todo) => (
|
|
103
|
-
<li
|
|
104
|
-
key={todo.id}
|
|
105
|
-
class={`todo-item ${todo.completed ? "completed" : ""}`}
|
|
106
|
-
>
|
|
107
|
-
<input
|
|
108
|
-
type="checkbox"
|
|
109
|
-
checked={todo.completed}
|
|
110
|
-
onChange={() => this.toggleTodo(todo.id)}
|
|
111
|
-
class="todo-checkbox"
|
|
112
|
-
/>
|
|
113
|
-
<span class="todo-text">{todo.text}</span>
|
|
114
|
-
<button
|
|
115
|
-
class="btn btn-sm btn-danger"
|
|
116
|
-
onClick={() => this.removeTodo(todo.id)}
|
|
117
|
-
>
|
|
118
|
-
Delete
|
|
119
|
-
</button>
|
|
120
|
-
</li>
|
|
121
|
-
))}
|
|
122
|
-
</ul>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{this.todos.length > 0 && (
|
|
127
|
-
<div class="todo-actions">
|
|
128
|
-
<button class="btn btn-warning" onClick={this.clearCompleted}>
|
|
129
|
-
Clear Completed
|
|
130
|
-
</button>
|
|
131
|
-
<button class="btn btn-danger" onClick={this.clearAll}>
|
|
132
|
-
Clear All
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
135
|
-
)}
|
|
136
|
-
|
|
137
|
-
<div class="debug-info">
|
|
138
|
-
<details>
|
|
139
|
-
<summary>Debug Info</summary>
|
|
140
|
-
<pre>
|
|
141
|
-
{JSON.stringify(
|
|
142
|
-
{
|
|
143
|
-
todosCount: this.todos.length,
|
|
144
|
-
filter: this.filter,
|
|
145
|
-
newTodoText: this._newTodoText,
|
|
146
|
-
todos: this.todos,
|
|
147
|
-
},
|
|
148
|
-
null,
|
|
149
|
-
2
|
|
150
|
-
)}
|
|
151
|
-
</pre>
|
|
152
|
-
</details>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private handleInputChange = (event: Event) => {
|
|
159
|
-
const input = event.target as HTMLInputElement;
|
|
160
|
-
// Update non-reactive state without triggering rerender
|
|
161
|
-
this._newTodoText = input.value;
|
|
162
|
-
// Update button disabled state directly without rerender
|
|
163
|
-
const button = this.shadowRoot?.querySelector(
|
|
164
|
-
".todo-input-section .btn-primary"
|
|
165
|
-
) as HTMLButtonElement;
|
|
166
|
-
if (button) {
|
|
167
|
-
button.disabled = !this._newTodoText.trim();
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
private handleKeyDown = (event: KeyboardEvent) => {
|
|
172
|
-
if (event.key === "Enter" && this._newTodoText.trim()) {
|
|
173
|
-
this.addTodo();
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
private addTodo = () => {
|
|
178
|
-
// Get input value directly from DOM to avoid state sync issues
|
|
179
|
-
const input = this.shadowRoot?.querySelector(".todo-input") as HTMLInputElement;
|
|
180
|
-
const text = input?.value.trim() || this._newTodoText.trim();
|
|
181
|
-
|
|
182
|
-
if (!text) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Array mutation - automatically reactive (triggers rerender)
|
|
187
|
-
this.todos = [
|
|
188
|
-
...this.todos,
|
|
189
|
-
{
|
|
190
|
-
id: this.nextId++,
|
|
191
|
-
text,
|
|
192
|
-
completed: false,
|
|
193
|
-
createdAt: Date.now(),
|
|
194
|
-
},
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
// Clear input - update both state and DOM
|
|
198
|
-
this._newTodoText = "";
|
|
199
|
-
if (input) {
|
|
200
|
-
input.value = "";
|
|
201
|
-
// Update button state
|
|
202
|
-
const button = this.shadowRoot?.querySelector(
|
|
203
|
-
".todo-input-section .btn-primary"
|
|
204
|
-
) as HTMLButtonElement;
|
|
205
|
-
if (button) {
|
|
206
|
-
button.disabled = true;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
logger.debug("Todo added", { count: this.todos.length });
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
private removeTodo = (id: number) => {
|
|
213
|
-
// Array filter - creates new array, automatically reactive
|
|
214
|
-
this.todos = this.todos.filter((todo) => todo.id !== id);
|
|
215
|
-
logger.debug("Todo removed", { id, remaining: this.todos.length });
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
private toggleTodo = (id: number) => {
|
|
219
|
-
// Array map - creates new array with updated item, automatically reactive
|
|
220
|
-
this.todos = this.todos.map((todo) =>
|
|
221
|
-
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
222
|
-
);
|
|
223
|
-
logger.debug("Todo toggled", { id });
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
private clearCompleted = () => {
|
|
227
|
-
// Array filter - removes all completed items
|
|
228
|
-
this.todos = this.todos.filter((todo) => !todo.completed);
|
|
229
|
-
logger.debug("Completed todos cleared");
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
private clearAll = () => {
|
|
233
|
-
// Array assignment - clears all items
|
|
234
|
-
this.todos = [];
|
|
235
|
-
logger.debug("All todos cleared");
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
private getFilteredTodos(): TodoItem[] {
|
|
239
|
-
switch (this.filter) {
|
|
240
|
-
case "active":
|
|
241
|
-
return this.todos.filter((todo) => !todo.completed);
|
|
242
|
-
case "completed":
|
|
243
|
-
return this.todos.filter((todo) => todo.completed);
|
|
244
|
-
default:
|
|
245
|
-
return this.todos;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private getActiveCount(): number {
|
|
250
|
-
return this.todos.filter((todo) => !todo.completed).length;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private getCompletedCount(): number {
|
|
254
|
-
return this.todos.filter((todo) => todo.completed).length;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
protected onConnected(): void {
|
|
258
|
-
logger.info("TodoList connected to DOM");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
protected onDisconnected(): void {
|
|
262
|
-
logger.info("TodoList disconnected from DOM");
|
|
263
|
-
}
|
|
264
|
-
}
|