@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.
Files changed (42) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +28 -28
  3. package/dist/index.cjs +14 -2
  4. package/dist/index.js +4971 -2032
  5. package/dist/style.css +1 -1
  6. package/package.json +16 -7
  7. package/src/{XyButton.css → Button.css} +1 -1
  8. package/src/{XyButton.wsx → Button.wsx} +18 -9
  9. package/src/ButtonGroup.css +30 -0
  10. package/src/{XyButtonGroup.wsx → ButtonGroup.wsx} +26 -14
  11. package/src/CodeBlock.css +275 -0
  12. package/src/CodeBlock.types.ts +25 -0
  13. package/src/CodeBlock.wsx +296 -0
  14. package/src/ColorPicker.wsx +6 -5
  15. package/src/Combobox.css +254 -0
  16. package/src/Combobox.types.ts +32 -0
  17. package/src/Combobox.wsx +352 -0
  18. package/src/Dropdown.css +178 -0
  19. package/src/Dropdown.types.ts +28 -0
  20. package/src/Dropdown.wsx +221 -0
  21. package/src/LanguageSwitcher.css +148 -0
  22. package/src/LanguageSwitcher.wsx +190 -0
  23. package/src/OverflowDetector.ts +169 -0
  24. package/src/ResponsiveNav.css +555 -0
  25. package/src/ResponsiveNav.types.ts +30 -0
  26. package/src/ResponsiveNav.wsx +450 -0
  27. package/src/SvgIcon.wsx +2 -2
  28. package/src/index.ts +17 -9
  29. package/src/types/wsx.d.ts +4 -3
  30. package/src/ReactiveCounter.css +0 -304
  31. package/src/ReactiveCounter.wsx +0 -231
  32. package/src/SimpleReactiveDemo.wsx +0 -59
  33. package/src/SvgDemo.wsx +0 -241
  34. package/src/TodoList.css +0 -197
  35. package/src/TodoList.wsx +0 -264
  36. package/src/TodoListLight.css +0 -198
  37. package/src/TodoListLight.wsx +0 -263
  38. package/src/UserProfile.css +0 -146
  39. package/src/UserProfile.wsx +0 -247
  40. package/src/UserProfileLight.css +0 -146
  41. package/src/UserProfileLight.wsx +0 -256
  42. package/src/XyButtonGroup.css +0 -30
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ResponsiveNav Types
3
+ * 响应式导航栏组件的类型定义
4
+ */
5
+
6
+ export interface NavItem {
7
+ /** 链接文本 */
8
+ label: string;
9
+ /** 链接路径 */
10
+ to: string;
11
+ /** 是否精确匹配 */
12
+ exact?: boolean;
13
+ /** 是否在移动端隐藏 */
14
+ hideOnMobile?: boolean;
15
+ }
16
+
17
+ export interface ResponsiveNavConfig {
18
+ /** 品牌名称 */
19
+ brand?: string;
20
+ /** 品牌图标(可选,可以是字符串或 HTMLElement) */
21
+ brandIcon?: HTMLElement | string;
22
+ /** 导航项列表 */
23
+ items: NavItem[];
24
+ /** 右侧操作项标签名列表(如 ['language-switcher', 'theme-switcher']) */
25
+ actionTags?: string[];
26
+ /** 移动端断点(px) */
27
+ mobileBreakpoint?: number;
28
+ /** 是否自动处理 overflow */
29
+ autoOverflow?: boolean;
30
+ }
@@ -0,0 +1,450 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * ResponsiveNav Component
4
+ * 响应式导航栏组件
5
+ * - 自动处理 overflow:当空间不足时,将导航项移到下拉菜单
6
+ * - 移动端自动切换为汉堡菜单
7
+ */
8
+ import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
9
+ import { OverflowDetector } from "./OverflowDetector";
10
+ import styles from "./ResponsiveNav.css?inline";
11
+ // 需要导入 wsx-link 和 wsx-router
12
+ // 注意:这些组件应该在外部已经注册
13
+
14
+ import type { NavItem, ResponsiveNavConfig } from "./ResponsiveNav.types";
15
+ export type { NavItem, ResponsiveNavConfig };
16
+
17
+ @autoRegister({ tagName: "wsx-responsive-nav" })
18
+ export default class ResponsiveNav extends WebComponent {
19
+ private navConfig: ResponsiveNavConfig = { items: [] };
20
+ /** 移动端菜单是否打开 */
21
+ @state private isMobileMenuOpen: boolean = false;
22
+ /** 可见的导航项索引 */
23
+ @state private visibleItemIndices: number[] = [];
24
+ /** 隐藏的导航项索引(在 overflow 菜单中) */
25
+ @state private hiddenItemIndices: number[] = [];
26
+ /** 是否移动端 */
27
+ @state private isMobile: boolean = false;
28
+ /** overflow 菜单是否打开 */
29
+ @state private isOverflowOpen: boolean = false;
30
+
31
+ /** 元素引用 */
32
+ private navMenuElement?: HTMLElement;
33
+ private navItemsElements: HTMLElement[] = [];
34
+ private resizeObserver?: ResizeObserver;
35
+ private resizeHandler?: () => void;
36
+
37
+ constructor(config?: ResponsiveNavConfig) {
38
+ super({
39
+ styles,
40
+ styleName: "wsx-responsive-nav",
41
+ });
42
+ // 如果通过构造函数传递配置,使用它;否则在 onConnected 时从属性读取
43
+ if (config) {
44
+ this.navConfig = {
45
+ mobileBreakpoint: 768,
46
+ autoOverflow: true,
47
+ ...config,
48
+ };
49
+ this.visibleItemIndices = config.items.map((_, index) => index);
50
+ }
51
+ }
52
+
53
+ render(): HTMLElement {
54
+ // 确保配置已初始化
55
+ if (!this.navConfig.items || this.navConfig.items.length === 0) {
56
+ return <nav class="responsive-nav"></nav>;
57
+ }
58
+
59
+ const hiddenItems = this.hiddenItemIndices.map((index) => this.navConfig.items[index]);
60
+
61
+ return (
62
+ <nav class="responsive-nav">
63
+ <div class="nav-container">
64
+ {/* 品牌 */}
65
+ <div class="nav-brand">
66
+ <slot name="brand-icon">
67
+ {this.navConfig.brandIcon &&
68
+ typeof this.navConfig.brandIcon === "string" && (
69
+ <span class="nav-brand-icon">{this.navConfig.brandIcon}</span>
70
+ )}
71
+ </slot>
72
+ {this.navConfig.brand && (
73
+ <span class="nav-brand-text">{this.navConfig.brand}</span>
74
+ )}
75
+ </div>
76
+
77
+ {/* 桌面端导航菜单 */}
78
+ {!this.isMobile && (
79
+ <div ref={(el) => (this.navMenuElement = el)} class="nav-menu">
80
+ {/* 渲染所有项,但隐藏不在 visibleItemIndices 中的项 */}
81
+ {this.navConfig.items.map((item, index) => {
82
+ const isVisible = this.visibleItemIndices.includes(index);
83
+ return (
84
+ <wsx-link
85
+ key={index}
86
+ ref={(el) => {
87
+ if (el) {
88
+ this.navItemsElements[index] = el;
89
+ }
90
+ }}
91
+ to={item.to}
92
+ class={`nav-link ${!isVisible ? "hidden-item" : ""}`}
93
+ active-class="nav-link-active"
94
+ exact={item.exact}
95
+ style={
96
+ !isVisible
97
+ ? "position: absolute; visibility: hidden; pointer-events: none;"
98
+ : ""
99
+ }
100
+ >
101
+ {item.label}
102
+ </wsx-link>
103
+ );
104
+ })}
105
+
106
+ {/* Overflow 菜单 */}
107
+ {this.navConfig.autoOverflow &&
108
+ hiddenItems.length > 0 &&
109
+ !this.isMobile && (
110
+ <div class="nav-overflow">
111
+ <button
112
+ class="nav-overflow-button"
113
+ onClick={this.toggleOverflow}
114
+ >
115
+ More ▼
116
+ </button>
117
+ {this.isOverflowOpen && (
118
+ <div class="nav-overflow-menu">
119
+ {hiddenItems.map((item, idx) => {
120
+ const originalIndex =
121
+ this.hiddenItemIndices[idx];
122
+ return (
123
+ <wsx-link
124
+ key={originalIndex}
125
+ to={item.to}
126
+ class="nav-overflow-link"
127
+ active-class="nav-link-active"
128
+ exact={item.exact}
129
+ onClick={this.closeOverflow}
130
+ >
131
+ {item.label}
132
+ </wsx-link>
133
+ );
134
+ })}
135
+ </div>
136
+ )}
137
+ </div>
138
+ )}
139
+ </div>
140
+ )}
141
+
142
+ {/* 右侧操作项(始终显示,包括移动端) */}
143
+ {this.navConfig.actionTags && (
144
+ <div class="nav-actions">
145
+ {this.navConfig.actionTags.map((tag, index) => {
146
+ // 使用动态标签名创建元素
147
+ const TagName = tag as keyof HTMLElementTagNameMap;
148
+ return (
149
+ <div key={index} class="nav-action">
150
+ <TagName></TagName>
151
+ </div>
152
+ );
153
+ })}
154
+ </div>
155
+ )}
156
+
157
+ {/* 移动端汉堡菜单按钮 */}
158
+ {this.isMobile && (
159
+ <button class="nav-toggle" onClick={this.toggleMobileMenu}>
160
+ <span
161
+ class={`nav-toggle-line ${this.isMobileMenuOpen ? "open" : ""}`}
162
+ ></span>
163
+ <span
164
+ class={`nav-toggle-line ${this.isMobileMenuOpen ? "open" : ""}`}
165
+ ></span>
166
+ <span
167
+ class={`nav-toggle-line ${this.isMobileMenuOpen ? "open" : ""}`}
168
+ ></span>
169
+ </button>
170
+ )}
171
+ </div>
172
+
173
+ {/* 移动端菜单 */}
174
+ {this.isMobile && (
175
+ <div class={`nav-mobile-menu ${this.isMobileMenuOpen ? "open" : ""}`}>
176
+ {this.navConfig.items
177
+ .filter((item) => !item.hideOnMobile)
178
+ .map((item, index) => (
179
+ <wsx-link
180
+ key={index}
181
+ to={item.to}
182
+ class="nav-mobile-link"
183
+ active-class="nav-link-active"
184
+ exact={item.exact}
185
+ onClick={this.closeMobileMenu}
186
+ >
187
+ {item.label}
188
+ </wsx-link>
189
+ ))}
190
+ </div>
191
+ )}
192
+ </nav>
193
+ );
194
+ }
195
+
196
+ /**
197
+ * 切换移动端菜单
198
+ */
199
+ private toggleMobileMenu = (): void => {
200
+ this.isMobileMenuOpen = !this.isMobileMenuOpen;
201
+ this.rerender();
202
+ };
203
+
204
+ /**
205
+ * 关闭移动端菜单
206
+ */
207
+ private closeMobileMenu = (): void => {
208
+ this.isMobileMenuOpen = false;
209
+ this.rerender();
210
+ };
211
+
212
+ /**
213
+ * 切换 overflow 菜单
214
+ */
215
+ private toggleOverflow = (): void => {
216
+ this.isOverflowOpen = !this.isOverflowOpen;
217
+ this.rerender();
218
+
219
+ if (this.isOverflowOpen) {
220
+ setTimeout(() => {
221
+ document.addEventListener("click", this.handleOverflowOutsideClick, true);
222
+ }, 0);
223
+ } else {
224
+ document.removeEventListener("click", this.handleOverflowOutsideClick, true);
225
+ }
226
+ };
227
+
228
+ /**
229
+ * 关闭 overflow 菜单
230
+ */
231
+ private closeOverflow = (): void => {
232
+ this.isOverflowOpen = false;
233
+ this.rerender();
234
+ document.removeEventListener("click", this.handleOverflowOutsideClick, true);
235
+ };
236
+
237
+ /**
238
+ * 处理 overflow 菜单外部点击
239
+ */
240
+ private handleOverflowOutsideClick = (event: Event): void => {
241
+ const target = event.target as Node;
242
+ const overflowContainer = this.querySelector(".nav-overflow");
243
+ const overflowMenu = overflowContainer?.querySelector(".nav-overflow-menu");
244
+ const overflowButton = overflowContainer?.querySelector(".nav-overflow-button");
245
+
246
+ if (
247
+ overflowMenu &&
248
+ overflowButton &&
249
+ !overflowMenu.contains(target) &&
250
+ !overflowButton.contains(target)
251
+ ) {
252
+ this.closeOverflow();
253
+ }
254
+ };
255
+
256
+ /**
257
+ * 检查并更新可见项
258
+ * 使用 OverflowDetector 来最大化可见项数量
259
+ */
260
+ private updateVisibleItems = (): void => {
261
+ if (!this.navConfig.autoOverflow || this.isMobile || !this.navMenuElement) {
262
+ // 如果不是移动端且 autoOverflow 关闭,显示所有项
263
+ if (!this.isMobile) {
264
+ const allIndices = this.navConfig.items.map((_, index) => index);
265
+ if (
266
+ JSON.stringify(allIndices.sort()) !==
267
+ JSON.stringify(this.visibleItemIndices.sort())
268
+ ) {
269
+ this.visibleItemIndices = allIndices;
270
+ this.hiddenItemIndices = [];
271
+ this.rerender();
272
+ }
273
+ }
274
+ return;
275
+ }
276
+
277
+ // 确保所有导航项元素都已获取
278
+ // 先渲染所有项(隐藏的项也需要渲染以获取宽度)
279
+ const allItems: HTMLElement[] = [];
280
+ for (let i = 0; i < this.navConfig.items.length; i++) {
281
+ // 尝试从已渲染的元素中获取
282
+ let itemElement = this.navItemsElements[i];
283
+ if (!itemElement) {
284
+ // 如果元素不存在,尝试从 DOM 中查找
285
+ const navLinks = Array.from(
286
+ this.navMenuElement.querySelectorAll(".nav-link")
287
+ ) as HTMLElement[];
288
+ if (navLinks[i]) {
289
+ itemElement = navLinks[i];
290
+ this.navItemsElements[i] = itemElement;
291
+ }
292
+ }
293
+ if (itemElement) {
294
+ allItems.push(itemElement);
295
+ } else {
296
+ // 如果元素还不存在,创建一个临时元素来测量宽度
297
+ // 这通常发生在首次渲染时
298
+ const tempElement = document.createElement("wsx-link");
299
+ tempElement.textContent = this.navConfig.items[i].label;
300
+ tempElement.style.visibility = "hidden";
301
+ tempElement.style.position = "absolute";
302
+ document.body.appendChild(tempElement);
303
+ allItems.push(tempElement);
304
+ // 清理临时元素
305
+ setTimeout(() => {
306
+ if (tempElement.parentElement) {
307
+ tempElement.parentElement.removeChild(tempElement);
308
+ }
309
+ }, 0);
310
+ }
311
+ }
312
+
313
+ // 计算其他元素占用的宽度
314
+ const actionsElement = this.navMenuElement.parentElement?.querySelector(
315
+ ".nav-actions"
316
+ ) as HTMLElement;
317
+ const actionsWidth = actionsElement
318
+ ? Array.from(actionsElement.children).reduce(
319
+ (sum, el) => sum + OverflowDetector.getElementTotalWidth(el as HTMLElement),
320
+ 0
321
+ ) + 8 // gap
322
+ : 0;
323
+
324
+ // 使用 OverflowDetector 计算可见/隐藏项
325
+ const overflowButtonWidth = 90; // overflow 按钮宽度(包括 padding 和 gap)
326
+ const gap = 16; // 导航项之间的间距
327
+
328
+ const result = OverflowDetector.detect({
329
+ container: this.navMenuElement,
330
+ items: allItems,
331
+ gap,
332
+ reservedWidth: actionsWidth,
333
+ overflowButtonWidth,
334
+ padding: 0,
335
+ minVisibleItems: 1,
336
+ });
337
+
338
+ // 更新状态
339
+ const newVisibleIndices = result.visibleIndices;
340
+ const newHiddenIndices = result.hiddenIndices;
341
+
342
+ // 清理临时元素
343
+ allItems.forEach((item) => {
344
+ if (item.parentElement === document.body) {
345
+ document.body.removeChild(item);
346
+ }
347
+ });
348
+
349
+ // 只有当结果发生变化时才更新
350
+ if (
351
+ JSON.stringify(newVisibleIndices.sort()) !==
352
+ JSON.stringify(this.visibleItemIndices.sort()) ||
353
+ JSON.stringify(newHiddenIndices.sort()) !==
354
+ JSON.stringify(this.hiddenItemIndices.sort())
355
+ ) {
356
+ this.visibleItemIndices = newVisibleIndices;
357
+ this.hiddenItemIndices = newHiddenIndices;
358
+ this.rerender();
359
+ }
360
+ };
361
+
362
+ /**
363
+ * 检查是否为移动端
364
+ */
365
+ private checkMobile = (): void => {
366
+ const isMobile = window.innerWidth <= (this.navConfig.mobileBreakpoint || 768);
367
+ if (isMobile !== this.isMobile) {
368
+ this.isMobile = isMobile;
369
+ this.rerender();
370
+ }
371
+ };
372
+
373
+ /**
374
+ * 组件连接时初始化
375
+ */
376
+ protected onConnected(): void {
377
+ // 如果配置未初始化,从属性读取
378
+ if (!this.navConfig.items || this.navConfig.items.length === 0) {
379
+ const configAttr = this.getAttribute("config");
380
+ if (configAttr) {
381
+ try {
382
+ const parsedConfig = JSON.parse(configAttr) as ResponsiveNavConfig;
383
+ this.navConfig = {
384
+ mobileBreakpoint: 768,
385
+ autoOverflow: true,
386
+ ...parsedConfig,
387
+ };
388
+ this.visibleItemIndices = parsedConfig.items.map((_, index) => index);
389
+ this.rerender();
390
+ } catch (error) {
391
+ console.error("Failed to parse ResponsiveNav config:", error);
392
+ }
393
+ }
394
+ }
395
+
396
+ this.checkMobile();
397
+
398
+ // 等待 DOM 渲染完成后再计算可见项
399
+ // 需要多次尝试,因为元素可能还没有渲染完成
400
+ const tryUpdateVisibleItems = (attempts: number = 0) => {
401
+ if (attempts > 10) return; // 最多尝试10次
402
+ setTimeout(() => {
403
+ if (this.navMenuElement) {
404
+ const hasAllElements = this.navConfig.items.every(
405
+ (_, index) => this.navItemsElements[index]
406
+ );
407
+ if (hasAllElements || attempts > 5) {
408
+ this.updateVisibleItems();
409
+ } else {
410
+ tryUpdateVisibleItems(attempts + 1);
411
+ }
412
+ }
413
+ }, 50);
414
+ };
415
+ tryUpdateVisibleItems();
416
+
417
+ // 监听窗口大小变化
418
+ this.resizeHandler = () => {
419
+ this.checkMobile();
420
+ setTimeout(() => {
421
+ this.updateVisibleItems();
422
+ }, 100);
423
+ };
424
+ window.addEventListener("resize", this.resizeHandler);
425
+
426
+ // 使用 ResizeObserver 监听容器大小变化
427
+ setTimeout(() => {
428
+ if (this.navMenuElement && window.ResizeObserver) {
429
+ this.resizeObserver = new ResizeObserver(() => {
430
+ setTimeout(() => {
431
+ this.updateVisibleItems();
432
+ }, 0);
433
+ });
434
+ this.resizeObserver.observe(this.navMenuElement);
435
+ }
436
+ }, 0);
437
+ }
438
+
439
+ /**
440
+ * 组件断开连接时清理
441
+ */
442
+ protected onDisconnected(): void {
443
+ if (this.resizeHandler) {
444
+ window.removeEventListener("resize", this.resizeHandler);
445
+ }
446
+ if (this.resizeObserver) {
447
+ this.resizeObserver.disconnect();
448
+ }
449
+ }
450
+ }
package/src/SvgIcon.wsx CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @jsxImportSource @wsxjs/wsx-core */
2
- import { WebComponent, autoRegister, createLogger } from "@wsxjs/wsx-core";
2
+ import { WebComponent, autoRegister } from "@wsxjs/wsx-core";
3
+ import { createLogger } from "@wsxjs/wsx-logger";
3
4
 
4
5
  const logger = createLogger("SvgIcon");
5
6
 
@@ -7,7 +8,6 @@ const logger = createLogger("SvgIcon");
7
8
  export default class SvgIcon extends WebComponent {
8
9
  constructor() {
9
10
  super();
10
- logger.info("SvgIcon component initialized");
11
11
  }
12
12
 
13
13
  render() {
package/src/index.ts CHANGED
@@ -1,21 +1,29 @@
1
1
  /** @jsxImportSource @wsxjs/wsx-core */
2
2
 
3
3
  // Export all base components (using default imports since they're default exports)
4
- export { default as XyButton } from "./XyButton.wsx";
5
- export { default as XyButtonGroup } from "./XyButtonGroup.wsx";
4
+ // Base components are reusable, generic components that can be used in any project
5
+ export { default as Button } from "./Button.wsx";
6
+ export { default as ButtonGroup } from "./ButtonGroup.wsx";
6
7
  export { default as ColorPicker } from "./ColorPicker.wsx";
7
- export { default as ReactiveCounter } from "./ReactiveCounter.wsx";
8
8
  export { default as ThemeSwitcher } from "./ThemeSwitcher.wsx";
9
+ export { default as Dropdown } from "./Dropdown.wsx";
10
+ export type { DropdownOption, DropdownConfig } from "./Dropdown.types";
11
+ export { default as Combobox } from "./Combobox.wsx";
12
+ export type { ComboboxConfig, ComboboxOption } from "./Combobox.types";
13
+ export { default as ResponsiveNav } from "./ResponsiveNav.wsx";
14
+ export type { NavItem, ResponsiveNavConfig } from "./ResponsiveNav.types";
9
15
  export { default as SvgIcon } from "./SvgIcon.wsx";
10
- export { default as SvgDemo } from "./SvgDemo.wsx";
11
- export { default as SimpleReactiveDemo } from "./SimpleReactiveDemo.wsx";
12
- export { default as TodoList } from "./TodoList.wsx";
13
- export { default as TodoListLight } from "./TodoListLight.wsx";
14
- export { default as UserProfile } from "./UserProfile.wsx";
15
- export { default as UserProfileLight } from "./UserProfileLight.wsx";
16
+ export { default as CodeBlock } from "./CodeBlock.wsx";
17
+ export type { CodeBlockConfig, CodeSegment } from "./CodeBlock.types";
18
+ export { OverflowDetector } from "./OverflowDetector";
19
+ export type { OverflowDetectorConfig, OverflowResult } from "./OverflowDetector";
16
20
 
17
21
  // Export utilities
18
22
  export * from "./ColorPickerUtils";
19
23
 
24
+ // Note: Example components (ReactiveCounter, TodoList, UserProfile, SlotCard, WsxLogo, etc.)
25
+ // have been moved to packages/examples/src/components/examples/
26
+ // Base components should only contain reusable, generic components
27
+
20
28
  // Note: Re-exports from core are causing TypeScript rootDir issues
21
29
  // Users can import directly from @wsxjs/wsx-core if needed
@@ -1,6 +1,7 @@
1
1
  // Type declaration for .wsx files
2
2
  declare module "*.wsx" {
3
- import { WebComponent } from "@wsxjs/wsx-core";
4
- const component: typeof WebComponent;
5
- export default component;
3
+ import { WebComponent, LightComponent } from "@wsxjs/wsx-core";
4
+ // Allow any class that extends WebComponent or LightComponent
5
+ const Component: new (...args: unknown[]) => WebComponent | LightComponent;
6
+ export default Component;
6
7
  }