@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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 插件类型定义
3
+ */
4
+ export type PluginType = "text" | "marker";
5
+
6
+ /**
7
+ * 节流函数类型定义
8
+ */
9
+ export type ThrottleFunction<T extends unknown[]> = (...args: T) => void;
10
+
11
+ const TEXT_COLOR_CACHE = "editor-js-text-color-cache";
12
+
13
+ /**
14
+ * 检查是否为CSS变量
15
+ * @param value 待检查的值
16
+ * @returns 是否为CSS变量
17
+ */
18
+ function isColorVariable(value: string): boolean {
19
+ return value.startsWith("var(") && value.endsWith(")");
20
+ }
21
+
22
+ /**
23
+ * 提取CSS变量名
24
+ * @param variable CSS变量字符串
25
+ * @returns 变量名
26
+ */
27
+ function extractVariableName(variable: string): string {
28
+ return variable.slice(4, -1).trim();
29
+ }
30
+
31
+ /**
32
+ * 获取CSS属性值
33
+ * @param propertyName CSS属性名
34
+ * @returns CSS属性值
35
+ */
36
+ function getCSSPropertyValue(propertyName: string): string {
37
+ const computedStyle = getComputedStyle(document.documentElement);
38
+ return computedStyle.getPropertyValue(propertyName).trim() || propertyName;
39
+ }
40
+
41
+ /**
42
+ * Convert CSS variables to color string.
43
+ * @param colorValue original value provided by users
44
+ * @returns string color string
45
+ */
46
+ export function handleCSSVariables(colorValue: string): string {
47
+ if (isColorVariable(colorValue)) {
48
+ const variableName = extractVariableName(colorValue);
49
+ return getCSSPropertyValue(variableName);
50
+ }
51
+ return colorValue;
52
+ }
53
+
54
+ /**
55
+ * Cache the latest text/marker color
56
+ * @param defaultColor 默认颜色值
57
+ * @param pluginType 插件类型
58
+ * @returns 返回设置的默认颜色
59
+ */
60
+ export function setDefaultColorCache(defaultColor: string, pluginType: PluginType): string {
61
+ sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}`, JSON.stringify(defaultColor));
62
+ return defaultColor;
63
+ }
64
+
65
+ /**
66
+ * Cache custom color
67
+ * @param customColor 自定义颜色值
68
+ * @param pluginType 插件类型
69
+ */
70
+ export function setCustomColorCache(customColor: string, pluginType: PluginType): void {
71
+ sessionStorage.setItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`, JSON.stringify(customColor));
72
+ }
73
+
74
+ /**
75
+ * Get cached custom color
76
+ * @param pluginType 插件类型
77
+ * @returns 缓存的自定义颜色值或null
78
+ */
79
+ export function getCustomColorCache(pluginType: PluginType): string | null {
80
+ const cachedCustomColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}-custom`);
81
+ return cachedCustomColor ? JSON.parse(cachedCustomColor) : null;
82
+ }
83
+
84
+ /**
85
+ * Throttle function
86
+ * @param fn 要节流的函数
87
+ * @param delay 延迟时间(毫秒)
88
+ * @returns 节流后的函数
89
+ */
90
+ export function throttle<T extends unknown[]>(
91
+ fn: ThrottleFunction<T>,
92
+ delay: number
93
+ ): (...args: T) => void {
94
+ let id: NodeJS.Timeout | null = null;
95
+ return (...args: T) => {
96
+ if (!id) {
97
+ id = setTimeout(() => {
98
+ fn(...args);
99
+ id = null;
100
+ }, delay);
101
+ }
102
+ };
103
+ }
104
+ /**
105
+ * Get cached text/marker color
106
+ * @param defaultColor 默认颜色值
107
+ * @param pluginType 插件类型
108
+ * @returns 缓存的颜色值或默认颜色
109
+ */
110
+ export function getDefaultColorCache(defaultColor: string, pluginType: PluginType): string {
111
+ const cachedDefaultColor = sessionStorage.getItem(`${TEXT_COLOR_CACHE}-${pluginType}`);
112
+ return cachedDefaultColor ? JSON.parse(cachedDefaultColor) : defaultColor;
113
+ }
114
+
115
+ export const CONVERTER_BTN = "ce-inline-toolbar__dropdown";
116
+ export const CONVERTER_PANEL = "ce-conversion-toolbar--showed";
@@ -0,0 +1,304 @@
1
+ .reactive-counter {
2
+ max-width: 500px;
3
+ margin: 20px auto;
4
+ padding: 24px;
5
+ border-radius: 12px;
6
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
8
+ transition: all 0.3s ease;
9
+ }
10
+
11
+ .theme-light {
12
+ background: #ffffff;
13
+ color: #333333;
14
+ border: 1px solid #e1e5e9;
15
+ }
16
+
17
+ .theme-dark {
18
+ background: #1a1a1a;
19
+ color: #ffffff;
20
+ border: 1px solid #333333;
21
+ }
22
+
23
+ .theme-blue {
24
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
+ color: #ffffff;
26
+ border: 1px solid #5a67d8;
27
+ }
28
+
29
+ .header {
30
+ text-align: center;
31
+ margin-bottom: 24px;
32
+ }
33
+
34
+ .header h3 {
35
+ margin: 0 0 8px 0;
36
+ font-size: 24px;
37
+ font-weight: 600;
38
+ }
39
+
40
+ .subtitle {
41
+ margin: 0;
42
+ opacity: 0.8;
43
+ font-size: 14px;
44
+ }
45
+
46
+ .counter-display {
47
+ text-align: center;
48
+ margin-bottom: 32px;
49
+ }
50
+
51
+ .count-value {
52
+ font-size: 48px;
53
+ font-weight: bold;
54
+ line-height: 1;
55
+ margin-bottom: 8px;
56
+ font-variant-numeric: tabular-nums;
57
+ }
58
+
59
+ .step-info {
60
+ font-size: 14px;
61
+ opacity: 0.7;
62
+ }
63
+
64
+ .controls {
65
+ display: flex;
66
+ gap: 12px;
67
+ justify-content: center;
68
+ margin-bottom: 24px;
69
+ flex-wrap: wrap;
70
+ }
71
+
72
+ .btn {
73
+ padding: 10px 20px;
74
+ border: none;
75
+ border-radius: 8px;
76
+ font-size: 16px;
77
+ font-weight: 500;
78
+ cursor: pointer;
79
+ transition: all 0.2s ease;
80
+ min-width: 60px;
81
+ }
82
+
83
+ .btn:disabled {
84
+ opacity: 0.5;
85
+ cursor: not-allowed;
86
+ }
87
+
88
+ .btn-primary {
89
+ background: #4299e1;
90
+ color: white;
91
+ }
92
+
93
+ .btn-primary:hover:not(:disabled) {
94
+ background: #3182ce;
95
+ transform: translateY(-1px);
96
+ }
97
+
98
+ .btn-secondary {
99
+ background: #718096;
100
+ color: white;
101
+ }
102
+
103
+ .btn-secondary:hover:not(:disabled) {
104
+ background: #4a5568;
105
+ transform: translateY(-1px);
106
+ }
107
+
108
+ .btn-warning {
109
+ background: #ed8936;
110
+ color: white;
111
+ }
112
+
113
+ .btn-warning:hover:not(:disabled) {
114
+ background: #dd6b20;
115
+ transform: translateY(-1px);
116
+ }
117
+
118
+ .btn-success {
119
+ background: #48bb78;
120
+ color: white;
121
+ }
122
+
123
+ .btn-success:hover:not(:disabled) {
124
+ background: #38a169;
125
+ transform: translateY(-1px);
126
+ }
127
+
128
+ .btn-danger {
129
+ background: #f56565;
130
+ color: white;
131
+ }
132
+
133
+ .btn-danger:hover:not(:disabled) {
134
+ background: #e53e3e;
135
+ transform: translateY(-1px);
136
+ }
137
+
138
+ .btn-sm {
139
+ padding: 6px 12px;
140
+ font-size: 14px;
141
+ min-width: auto;
142
+ }
143
+
144
+ .step-controls,
145
+ .auto-controls,
146
+ .theme-controls,
147
+ .message-controls {
148
+ margin-bottom: 20px;
149
+ text-align: center;
150
+ }
151
+
152
+ .step-controls label {
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ gap: 12px;
157
+ font-size: 14px;
158
+ }
159
+
160
+ .step-controls input[type="range"] {
161
+ flex: 0 0 150px;
162
+ }
163
+
164
+ .step-controls span {
165
+ font-weight: bold;
166
+ min-width: 20px;
167
+ }
168
+
169
+ .theme-controls select,
170
+ .message-controls input {
171
+ padding: 8px 12px;
172
+ border: 1px solid #cbd5e0;
173
+ border-radius: 6px;
174
+ font-size: 14px;
175
+ margin-left: 8px;
176
+ }
177
+
178
+ .theme-dark .theme-controls select,
179
+ .theme-dark .message-controls input {
180
+ background: #2d3748;
181
+ border-color: #4a5568;
182
+ color: #ffffff;
183
+ }
184
+
185
+ .theme-blue .theme-controls select,
186
+ .theme-blue .message-controls input {
187
+ background: rgba(255, 255, 255, 0.1);
188
+ border-color: rgba(255, 255, 255, 0.2);
189
+ color: #ffffff;
190
+ }
191
+
192
+ .message-controls input {
193
+ width: 250px;
194
+ max-width: 100%;
195
+ }
196
+
197
+ .history {
198
+ margin-top: 24px;
199
+ padding: 16px;
200
+ background: rgba(0, 0, 0, 0.05);
201
+ border-radius: 8px;
202
+ }
203
+
204
+ .theme-dark .history {
205
+ background: rgba(255, 255, 255, 0.05);
206
+ }
207
+
208
+ .theme-blue .history {
209
+ background: rgba(255, 255, 255, 0.1);
210
+ }
211
+
212
+ .history h4 {
213
+ margin: 0 0 12px 0;
214
+ font-size: 16px;
215
+ }
216
+
217
+ .history-list {
218
+ display: flex;
219
+ flex-wrap: wrap;
220
+ gap: 8px;
221
+ margin-bottom: 12px;
222
+ }
223
+
224
+ .history-item {
225
+ padding: 4px 8px;
226
+ background: rgba(66, 153, 225, 0.1);
227
+ border: 1px solid rgba(66, 153, 225, 0.2);
228
+ border-radius: 4px;
229
+ font-size: 12px;
230
+ font-weight: 500;
231
+ }
232
+
233
+ .debug-info {
234
+ margin-top: 24px;
235
+ font-size: 12px;
236
+ }
237
+
238
+ .debug-info details {
239
+ border: 1px solid #e2e8f0;
240
+ border-radius: 6px;
241
+ padding: 8px;
242
+ }
243
+
244
+ .theme-dark .debug-info details {
245
+ border-color: #4a5568;
246
+ }
247
+
248
+ .theme-blue .debug-info details {
249
+ border-color: rgba(255, 255, 255, 0.2);
250
+ }
251
+
252
+ .debug-info summary {
253
+ cursor: pointer;
254
+ font-weight: 500;
255
+ margin-bottom: 8px;
256
+ }
257
+
258
+ .debug-info pre {
259
+ margin: 0;
260
+ padding: 8px;
261
+ background: rgba(0, 0, 0, 0.05);
262
+ border-radius: 4px;
263
+ overflow-x: auto;
264
+ font-size: 11px;
265
+ line-height: 1.4;
266
+ }
267
+
268
+ .theme-dark .debug-info pre {
269
+ background: rgba(255, 255, 255, 0.05);
270
+ }
271
+
272
+ .theme-blue .debug-info pre {
273
+ background: rgba(255, 255, 255, 0.1);
274
+ }
275
+
276
+ /* 响应式设计 */
277
+ @media (max-width: 600px) {
278
+ .reactive-counter {
279
+ margin: 10px;
280
+ padding: 16px;
281
+ }
282
+
283
+ .count-value {
284
+ font-size: 36px;
285
+ }
286
+
287
+ .controls {
288
+ flex-direction: column;
289
+ align-items: center;
290
+ }
291
+
292
+ .btn {
293
+ width: 200px;
294
+ }
295
+
296
+ .step-controls label {
297
+ flex-direction: column;
298
+ gap: 8px;
299
+ }
300
+
301
+ .message-controls input {
302
+ width: 100%;
303
+ }
304
+ }
@@ -0,0 +1,244 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * 响应式计数器组件 - 展示 WSX 响应式状态系统
4
+ *
5
+ * 这个组件演示了:
6
+ * - 自动重渲染:状态变化时无需手动调用 rerender()
7
+ * - 批量更新:多个状态变化会被合并为一次重渲染
8
+ * - 原生性能:基于浏览器 Proxy API,零运行时开销
9
+ */
10
+
11
+ import { ReactiveWebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
12
+ import styles from "./ReactiveCounter.css?inline";
13
+
14
+ const logger = createLogger("ReactiveCounter");
15
+
16
+ @autoRegister({ tagName: "reactive-counter" })
17
+ export default class ReactiveCounter extends ReactiveWebComponent {
18
+ // 使用响应式对象 - 任何属性变化都会自动触发重渲染
19
+ private state = this.reactive({
20
+ count: 0,
21
+ step: 1,
22
+ message: "Hello WSX Reactive!",
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[]);
29
+
30
+ private getTheme = this.themeState[0];
31
+ private setTheme = this.themeState[1];
32
+ private getHistory = this.historyState[0];
33
+ private setHistory = this.historyState[1];
34
+
35
+ constructor() {
36
+ super({
37
+ styles,
38
+ debug: true, // 启用调试模式
39
+ });
40
+
41
+ logger.info("ReactiveCounter initialized");
42
+ }
43
+
44
+ render() {
45
+ const theme = this.getTheme();
46
+ const history = this.getHistory();
47
+
48
+ return (
49
+ <div class={`reactive-counter theme-${theme}`}>
50
+ <div class="header">
51
+ <h3>🔄 Reactive Counter</h3>
52
+ <p class="subtitle">{this.state.message}</p>
53
+ </div>
54
+
55
+ <div class="counter-display">
56
+ <div class="count-value">{this.state.count}</div>
57
+ <div class="step-info">Step: {this.state.step}</div>
58
+ </div>
59
+
60
+ <div class="controls">
61
+ <button
62
+ class="btn btn-primary"
63
+ onClick={this.increment}
64
+ disabled={this.state.isRunning}
65
+ >
66
+ +{this.state.step}
67
+ </button>
68
+
69
+ <button
70
+ class="btn btn-secondary"
71
+ onClick={this.decrement}
72
+ disabled={this.state.isRunning}
73
+ >
74
+ -{this.state.step}
75
+ </button>
76
+
77
+ <button
78
+ class="btn btn-warning"
79
+ onClick={this.reset}
80
+ disabled={this.state.isRunning}
81
+ >
82
+ Reset
83
+ </button>
84
+ </div>
85
+
86
+ <div class="step-controls">
87
+ <label>
88
+ Step Size:
89
+ <input
90
+ type="range"
91
+ min="1"
92
+ max="10"
93
+ value={this.state.step}
94
+ onInput={this.handleStepChange}
95
+ />
96
+ <span>{this.state.step}</span>
97
+ </label>
98
+ </div>
99
+
100
+ <div class="auto-controls">
101
+ <button
102
+ class={`btn ${this.state.isRunning ? "btn-danger" : "btn-success"}`}
103
+ onClick={this.toggleAutoIncrement}
104
+ >
105
+ {this.state.isRunning ? "Stop" : "Start"} Auto Increment
106
+ </button>
107
+ </div>
108
+
109
+ <div class="theme-controls">
110
+ <label>
111
+ Theme:
112
+ <select value={theme} onChange={this.handleThemeChange}>
113
+ <option value="light">Light</option>
114
+ <option value="dark">Dark</option>
115
+ <option value="blue">Blue</option>
116
+ </select>
117
+ </label>
118
+ </div>
119
+
120
+ <div class="message-controls">
121
+ <input
122
+ type="text"
123
+ value={this.state.message}
124
+ onInput={this.handleMessageChange}
125
+ placeholder="Enter a message..."
126
+ />
127
+ </div>
128
+
129
+ {history.length > 0 && (
130
+ <div class="history">
131
+ <h4>History (last 10):</h4>
132
+ <div class="history-list">
133
+ {history.slice(-10).map((value, index) => (
134
+ <span key={index} class="history-item">
135
+ {value}
136
+ </span>
137
+ ))}
138
+ </div>
139
+ <button class="btn btn-sm" onClick={this.clearHistory}>
140
+ Clear History
141
+ </button>
142
+ </div>
143
+ )}
144
+
145
+ <div class="debug-info">
146
+ <details>
147
+ <summary>Debug Info</summary>
148
+ <pre>
149
+ {JSON.stringify(
150
+ {
151
+ state: this.state,
152
+ theme: this.getTheme(),
153
+ historyLength: this.getHistory().length,
154
+ stateSnapshot: this.getStateSnapshot(),
155
+ },
156
+ null,
157
+ 2
158
+ )}
159
+ </pre>
160
+ </details>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ // 事件处理方法 - 直接修改状态,自动触发重渲染
167
+ private increment = () => {
168
+ this.state.count += this.state.step;
169
+ this.addToHistory(this.state.count);
170
+ };
171
+
172
+ private decrement = () => {
173
+ this.state.count -= this.state.step;
174
+ this.addToHistory(this.state.count);
175
+ };
176
+
177
+ private reset = () => {
178
+ this.state.count = 0;
179
+ this.addToHistory(0);
180
+ };
181
+
182
+ private handleStepChange = (event: Event) => {
183
+ const input = event.target as HTMLInputElement;
184
+ this.state.step = parseInt(input.value);
185
+ };
186
+
187
+ private handleThemeChange = (event: Event) => {
188
+ const select = event.target as HTMLSelectElement;
189
+ this.setTheme(select.value);
190
+ };
191
+
192
+ private handleMessageChange = (event: Event) => {
193
+ const input = event.target as HTMLInputElement;
194
+ this.state.message = input.value;
195
+ };
196
+
197
+ private toggleAutoIncrement = () => {
198
+ this.state.isRunning = !this.state.isRunning;
199
+
200
+ if (this.state.isRunning) {
201
+ this.startAutoIncrement();
202
+ }
203
+ };
204
+
205
+ private startAutoIncrement() {
206
+ const interval = setInterval(() => {
207
+ if (!this.state.isRunning) {
208
+ clearInterval(interval);
209
+ return;
210
+ }
211
+
212
+ this.increment();
213
+
214
+ // 演示批量更新:这些变化会被合并为一次重渲染
215
+ if (this.state.count % 5 === 0) {
216
+ this.state.message = `Count reached ${this.state.count}!`;
217
+ }
218
+ }, 200);
219
+ }
220
+
221
+ private addToHistory(value: number) {
222
+ const history = this.getHistory();
223
+ this.setHistory([...history, value]);
224
+ }
225
+
226
+ private clearHistory = () => {
227
+ this.setHistory([]);
228
+ };
229
+
230
+ protected onConnected(): void {
231
+ logger.info("ReactiveCounter connected to DOM");
232
+
233
+ // 组件挂载时显示欢迎消息
234
+ setTimeout(() => {
235
+ this.state.message = "Ready to count! 🚀";
236
+ }, 500);
237
+ }
238
+
239
+ protected onDisconnected(): void {
240
+ logger.info("ReactiveCounter disconnected from DOM");
241
+ // 清理定时器等资源
242
+ this.state.isRunning = false;
243
+ }
244
+ }
@@ -0,0 +1,58 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * 简单的响应式演示组件
4
+ * 展示 WSX 响应式状态系统的基本功能
5
+ */
6
+
7
+ import { ReactiveWebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
8
+
9
+ const logger = createLogger("SimpleReactiveDemo");
10
+
11
+ @autoRegister({ tagName: "simple-reactive-demo" })
12
+ export default class SimpleReactiveDemo extends ReactiveWebComponent {
13
+ // 响应式状态 - 自动触发重渲染
14
+ private state = this.reactive({
15
+ count: 0,
16
+ message: "Click the button!",
17
+ });
18
+
19
+ constructor() {
20
+ super();
21
+ logger.info("SimpleReactiveDemo initialized");
22
+ }
23
+
24
+ render() {
25
+ return (
26
+ <div style="padding: 20px; border: 1px solid #ccc; border-radius: 8px; margin: 20px;">
27
+ <h3>📱 Simple Reactive Demo</h3>
28
+ <p>{this.state.message}</p>
29
+ <div style="font-size: 24px; margin: 16px 0;">
30
+ Count: <strong>{this.state.count}</strong>
31
+ </div>
32
+ <div>
33
+ <button onClick={this.increment} style="margin-right: 8px; padding: 8px 16px;">
34
+ + Increment
35
+ </button>
36
+ <button onClick={this.reset} style="padding: 8px 16px;">
37
+ 🔄 Reset
38
+ </button>
39
+ </div>
40
+ <div style="margin-top: 16px; font-size: 12px; color: #666;">
41
+ 💡 State changes automatically trigger re-renders!
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ private increment = () => {
48
+ this.state.count++;
49
+ this.state.message = `Clicked ${this.state.count} time${this.state.count === 1 ? "" : "s"}!`;
50
+ // 注意:不需要手动调用 this.rerender()!
51
+ };
52
+
53
+ private reset = () => {
54
+ this.state.count = 0;
55
+ this.state.message = "Reset! Click again!";
56
+ // 注意:这里也不需要手动调用 this.rerender()!
57
+ };
58
+ }