@wsxjs/wsx-base-components 0.0.7 → 0.0.9
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/dist/index.cjs +2 -2
- package/dist/index.js +2284 -756
- package/dist/style.css +1 -0
- package/package.json +4 -4
- package/src/ColorPicker.wsx +0 -2
- package/src/ReactiveCounter.wsx +19 -32
- package/src/SimpleReactiveDemo.wsx +6 -5
- package/src/ThemeSwitcher.wsx +0 -2
- package/src/TodoList.css +197 -0
- package/src/TodoList.wsx +264 -0
- package/src/TodoListLight.css +198 -0
- package/src/TodoListLight.wsx +263 -0
- package/src/UserProfile.css +146 -0
- package/src/UserProfile.wsx +247 -0
- package/src/UserProfileLight.css +146 -0
- package/src/UserProfileLight.wsx +256 -0
- package/src/XyButton.wsx +0 -2
- package/src/XyButtonGroup.wsx +0 -2
- package/src/index.ts +4 -0
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.todo-list-light{padding:1.5rem;max-width:600px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.todo-header{margin-bottom:1.5rem;text-align:center}.todo-header h2{margin:0 0 .5rem;color:#2196f3;font-size:1.75rem}.subtitle{margin:0;color:#666;font-size:.9rem}.todo-input-section{display:flex;gap:.5rem;margin-bottom:1rem}.todo-input{flex:1;padding:.75rem;border:2px solid #ddd;border-radius:4px;font-size:1rem;transition:border-color .2s}.todo-input:focus{outline:none;border-color:#2196f3}.todo-filters{display:flex;gap:.5rem;margin-bottom:1rem;justify-content:center}.filter-btn{padding:.5rem 1rem;border:2px solid #ddd;background:#fff;border-radius:4px;cursor:pointer;transition:all .2s;font-size:.9rem}.filter-btn:hover{background:#f5f5f5}.filter-btn.active{background:#2196f3;color:#fff;border-color:#2196f3}.todo-list-container{min-height:200px;margin-bottom:1rem}.empty-state{text-align:center;padding:3rem 1rem;color:#999;font-style:italic}.todo-items{list-style:none;padding:0;margin:0}.todo-item{display:flex;align-items:center;gap:.75rem;padding:.75rem;margin-bottom:.5rem;background:#f0f7ff;border-radius:4px;transition:background .2s}.todo-item:hover{background:#e3f2fd}.todo-item.completed{opacity:.6}.todo-checkbox{width:1.25rem;height:1.25rem;cursor:pointer}.todo-text{flex:1;font-size:1rem}.todo-item.completed .todo-text{text-decoration:line-through;color:#999}.todo-actions{display:flex;gap:.5rem;justify-content:center;margin-top:1rem}.btn{padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;font-size:.9rem;transition:all .2s}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-primary{background:#2196f3;color:#fff}.btn-primary:hover:not(:disabled){background:#1976d2}.btn-warning{background:#ff9800;color:#fff}.btn-warning:hover{background:#e68900}.btn-danger{background:#f44336;color:#fff}.btn-danger:hover{background:#da190b}.btn-sm{padding:.25rem .5rem;font-size:.8rem}.debug-info{margin-top:2rem;padding-top:1rem;border-top:1px solid #ddd}.debug-info summary{cursor:pointer;color:#666;font-size:.9rem}.debug-info pre{background:#f5f5f5;padding:1rem;border-radius:4px;overflow-x:auto;font-size:.85rem;margin-top:.5rem}.user-profile{padding:1.5rem;max-width:800px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.profile-header{margin-bottom:2rem;text-align:center}.profile-header h2{margin:0 0 .5rem;color:#333;font-size:1.75rem}.profile-header .subtitle{color:#666;font-size:.9rem;margin:0}.profile-content{display:flex;flex-direction:column;gap:2rem}.profile-section{background:#f8f9fa;padding:1.5rem;border-radius:8px;border:1px solid #e0e0e0}.profile-section h3{margin:0 0 1rem;color:#333;font-size:1.25rem;border-bottom:2px solid #007bff;padding-bottom:.5rem}.form-group{margin-bottom:1rem}.form-group label{display:block;margin-bottom:.5rem;color:#555;font-weight:500;font-size:.9rem}.form-group input[type=checkbox]{margin-right:.5rem}.input-field{width:100%;padding:.75rem;border:1px solid #ddd;border-radius:4px;font-size:1rem;transition:border-color .2s;box-sizing:border-box}.input-field:focus{outline:none;border-color:#007bff;box-shadow:0 0 0 3px #007bff1a}.profile-actions{display:flex;gap:1rem;flex-wrap:wrap}.btn{padding:.75rem 1.5rem;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:all .2s;font-weight:500}.btn-primary{background:#007bff;color:#fff}.btn-primary:hover{background:#0056b3}.btn-secondary{background:#6c757d;color:#fff}.btn-secondary:hover{background:#545b62}.btn-warning{background:#ffc107;color:#212529}.btn-warning:hover{background:#e0a800}.profile-display{background:#f8f9fa;padding:1.5rem;border-radius:8px;border:1px solid #e0e0e0}.profile-display h3{margin:0 0 1rem;color:#333;font-size:1.25rem}.json-display{background:#2d2d2d;color:#f8f8f2;padding:1rem;border-radius:4px;overflow-x:auto;font-family:Courier New,monospace;font-size:.875rem;line-height:1.5;margin:0;white-space:pre-wrap;word-wrap:break-word}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/wsx-base-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Base UI components built with WSX Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"!**/test"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@wsxjs/wsx-core": "0.0.
|
|
21
|
+
"@wsxjs/wsx-core": "0.0.9"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tsup": "^8.0.0",
|
|
29
29
|
"typescript": "^5.0.0",
|
|
30
30
|
"vite": "^5.4.19",
|
|
31
|
-
"@wsxjs/eslint-plugin-wsx": "0.0.
|
|
32
|
-
"@wsxjs/wsx-vite-plugin": "0.0.
|
|
31
|
+
"@wsxjs/eslint-plugin-wsx": "0.0.9",
|
|
32
|
+
"@wsxjs/wsx-vite-plugin": "0.0.9"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"wsx",
|
package/src/ColorPicker.wsx
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { WebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
13
|
-
import styles from "./ColorPicker.css?inline";
|
|
14
13
|
import {
|
|
15
14
|
handleCSSVariables,
|
|
16
15
|
setDefaultColorCache,
|
|
@@ -89,7 +88,6 @@ export default class ColorPicker extends WebComponent {
|
|
|
89
88
|
|
|
90
89
|
constructor(config: ColorPickerConfig = {}) {
|
|
91
90
|
super({
|
|
92
|
-
styles,
|
|
93
91
|
styleName: "base-color-picker",
|
|
94
92
|
...config,
|
|
95
93
|
});
|
package/src/ReactiveCounter.wsx
CHANGED
|
@@ -6,47 +6,36 @@
|
|
|
6
6
|
* - 自动重渲染:状态变化时无需手动调用 rerender()
|
|
7
7
|
* - 批量更新:多个状态变化会被合并为一次重渲染
|
|
8
8
|
* - 原生性能:基于浏览器 Proxy API,零运行时开销
|
|
9
|
+
* - 使用 @state 装饰器:自动初始化响应式状态
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
-
import styles from "./ReactiveCounter.css?inline";
|
|
12
|
+
import { WebComponent, state, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
13
13
|
|
|
14
14
|
const logger = createLogger("ReactiveCounter");
|
|
15
15
|
|
|
16
16
|
@autoRegister({ tagName: "reactive-counter" })
|
|
17
|
-
export default class ReactiveCounter extends
|
|
18
|
-
//
|
|
19
|
-
private state =
|
|
17
|
+
export default class ReactiveCounter extends WebComponent {
|
|
18
|
+
// 使用 @state 装饰器 - 自动初始化为响应式状态
|
|
19
|
+
@state private state = {
|
|
20
20
|
count: 0,
|
|
21
21
|
step: 1,
|
|
22
22
|
message: "Hello WSX Reactive!",
|
|
23
23
|
isRunning: false,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 使用 useState API - 类似 React 的 useState
|
|
27
|
-
private themeState = this.useState("theme", "light");
|
|
28
|
-
private historyState = this.useState("history", [] as number[]);
|
|
24
|
+
};
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private setHistory = this.historyState[1];
|
|
26
|
+
// 使用 @state 装饰器 - 基本类型自动使用 useState
|
|
27
|
+
@state private theme = "light";
|
|
28
|
+
@state private history: number[] = [];
|
|
34
29
|
|
|
35
30
|
constructor() {
|
|
36
|
-
super(
|
|
37
|
-
styles,
|
|
38
|
-
debug: true, // 启用调试模式
|
|
39
|
-
});
|
|
31
|
+
super();
|
|
40
32
|
|
|
41
33
|
logger.info("ReactiveCounter initialized");
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
render() {
|
|
45
|
-
const theme = this.getTheme();
|
|
46
|
-
const history = this.getHistory();
|
|
47
|
-
|
|
48
37
|
return (
|
|
49
|
-
<div class={`reactive-counter theme-${theme}`}>
|
|
38
|
+
<div class={`reactive-counter theme-${this.theme}`}>
|
|
50
39
|
<div class="header">
|
|
51
40
|
<h3>🔄 Reactive Counter</h3>
|
|
52
41
|
<p class="subtitle">{this.state.message}</p>
|
|
@@ -109,7 +98,7 @@ export default class ReactiveCounter extends ReactiveWebComponent {
|
|
|
109
98
|
<div class="theme-controls">
|
|
110
99
|
<label>
|
|
111
100
|
Theme:
|
|
112
|
-
<select value={theme} onChange={this.handleThemeChange}>
|
|
101
|
+
<select value={this.theme} onChange={this.handleThemeChange}>
|
|
113
102
|
<option value="light">Light</option>
|
|
114
103
|
<option value="dark">Dark</option>
|
|
115
104
|
<option value="blue">Blue</option>
|
|
@@ -126,11 +115,11 @@ export default class ReactiveCounter extends ReactiveWebComponent {
|
|
|
126
115
|
/>
|
|
127
116
|
</div>
|
|
128
117
|
|
|
129
|
-
{history.length > 0 && (
|
|
118
|
+
{this.history.length > 0 && (
|
|
130
119
|
<div class="history">
|
|
131
120
|
<h4>History (last 10):</h4>
|
|
132
121
|
<div class="history-list">
|
|
133
|
-
{history.slice(-10).map((value, index) => (
|
|
122
|
+
{this.history.slice(-10).map((value, index) => (
|
|
134
123
|
<span key={index} class="history-item">
|
|
135
124
|
{value}
|
|
136
125
|
</span>
|
|
@@ -149,9 +138,8 @@ export default class ReactiveCounter extends ReactiveWebComponent {
|
|
|
149
138
|
{JSON.stringify(
|
|
150
139
|
{
|
|
151
140
|
state: this.state,
|
|
152
|
-
theme: this.
|
|
153
|
-
historyLength: this.
|
|
154
|
-
stateSnapshot: this.getStateSnapshot(),
|
|
141
|
+
theme: this.theme,
|
|
142
|
+
historyLength: this.history.length,
|
|
155
143
|
},
|
|
156
144
|
null,
|
|
157
145
|
2
|
|
@@ -186,7 +174,7 @@ export default class ReactiveCounter extends ReactiveWebComponent {
|
|
|
186
174
|
|
|
187
175
|
private handleThemeChange = (event: Event) => {
|
|
188
176
|
const select = event.target as HTMLSelectElement;
|
|
189
|
-
this.
|
|
177
|
+
this.theme = select.value;
|
|
190
178
|
};
|
|
191
179
|
|
|
192
180
|
private handleMessageChange = (event: Event) => {
|
|
@@ -219,12 +207,11 @@ export default class ReactiveCounter extends ReactiveWebComponent {
|
|
|
219
207
|
}
|
|
220
208
|
|
|
221
209
|
private addToHistory(value: number) {
|
|
222
|
-
|
|
223
|
-
this.setHistory([...history, value]);
|
|
210
|
+
this.history = [...this.history, value];
|
|
224
211
|
}
|
|
225
212
|
|
|
226
213
|
private clearHistory = () => {
|
|
227
|
-
this.
|
|
214
|
+
this.history = [];
|
|
228
215
|
};
|
|
229
216
|
|
|
230
217
|
protected onConnected(): void {
|
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 简单的响应式演示组件
|
|
4
4
|
* 展示 WSX 响应式状态系统的基本功能
|
|
5
|
+
* 使用 @state 装饰器自动初始化响应式状态
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { WebComponent, state, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
8
9
|
|
|
9
10
|
const logger = createLogger("SimpleReactiveDemo");
|
|
10
11
|
|
|
11
12
|
@autoRegister({ tagName: "simple-reactive-demo" })
|
|
12
|
-
export default class SimpleReactiveDemo extends
|
|
13
|
-
//
|
|
14
|
-
private state =
|
|
13
|
+
export default class SimpleReactiveDemo extends WebComponent {
|
|
14
|
+
// 使用 @state 装饰器 - 自动初始化为响应式状态
|
|
15
|
+
@state private state = {
|
|
15
16
|
count: 0,
|
|
16
17
|
message: "Click the button!",
|
|
17
|
-
}
|
|
18
|
+
};
|
|
18
19
|
|
|
19
20
|
constructor() {
|
|
20
21
|
super();
|
package/src/ThemeSwitcher.wsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
2
|
|
|
3
3
|
import { WebComponent, autoRegister } from "@wsxjs/wsx-core";
|
|
4
|
-
import styles from "./ThemeSwitcher.css?inline";
|
|
5
4
|
|
|
6
5
|
@autoRegister({ tagName: "theme-switcher" })
|
|
7
6
|
export default class ThemeSwitcher extends WebComponent {
|
|
@@ -9,7 +8,6 @@ export default class ThemeSwitcher extends WebComponent {
|
|
|
9
8
|
|
|
10
9
|
constructor() {
|
|
11
10
|
super({
|
|
12
|
-
styles,
|
|
13
11
|
styleName: "theme-switcher",
|
|
14
12
|
});
|
|
15
13
|
|
package/src/TodoList.css
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
}
|