@windrun-huaiin/third-ui 5.9.5 → 5.9.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "5.9.5",
3
+ "version": "5.9.7",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -53,7 +53,7 @@
53
53
  "mermaid": "^11.6.0",
54
54
  "react-medium-image-zoom": "^5.2.14",
55
55
  "zod": "^3.22.4",
56
- "@windrun-huaiin/base-ui": "^5.3.3"
56
+ "@windrun-huaiin/base-ui": "^5.3.4"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": "19.1.0",
@@ -11,15 +11,12 @@ export interface GradientButtonProps {
11
11
  align?: 'left' | 'center' | 'right';
12
12
  disabled?: boolean;
13
13
  className?: string;
14
-
15
- // 跳转模式
14
+ // for Link
16
15
  href?: string;
17
16
  openInNewTab?: boolean;
18
17
 
19
- // 点击模式
18
+ // for click
20
19
  onClick?: () => void | Promise<void>;
21
-
22
- // 加载状态配置
23
20
  loadingText?: React.ReactNode;
24
21
  preventDoubleClick?: boolean;
25
22
  }
@@ -33,10 +30,11 @@ export function GradientButton({
33
30
  href,
34
31
  openInNewTab = true,
35
32
  onClick,
36
- loadingText = "Loading...",
33
+ loadingText,
37
34
  preventDoubleClick = true,
38
35
  }: GradientButtonProps) {
39
36
  const [isLoading, setIsLoading] = useState(false);
37
+ const actualLoadingText = loadingText || title?.toString().trim() || 'Loading...'
40
38
 
41
39
  // set justify class according to alignment
42
40
  const getAlignmentClass = () => {
@@ -50,7 +48,6 @@ export function GradientButton({
50
48
  }
51
49
  };
52
50
 
53
- // 处理点击事件
54
51
  const handleClick = async (e: React.MouseEvent) => {
55
52
  if (disabled || isLoading) {
56
53
  e.preventDefault();
@@ -76,13 +73,11 @@ export function GradientButton({
76
73
  }
77
74
  };
78
75
 
79
- // 按钮是否处于禁用状态
80
76
  const isDisabled = disabled || isLoading;
81
77
 
82
- // 显示的标题内容
83
- const displayTitle = isLoading ? loadingText : title;
78
+ const displayTitle = isLoading ? actualLoadingText : title;
84
79
 
85
- // 显示的图标
80
+ // icon
86
81
  const displayIcon = isLoading ? (
87
82
  <icons.Loader2 className="h-4 w-4 text-white animate-spin" />
88
83
  ) : icon ? (
@@ -93,7 +88,12 @@ export function GradientButton({
93
88
  <icons.ArrowRight className="h-4 w-4 text-white" />
94
89
  );
95
90
 
96
- const buttonContent = (
91
+ const buttonContent = onClick ? (
92
+ <>
93
+ <span>{displayIcon}</span>
94
+ <span className="ml-1">{displayTitle}</span>
95
+ </>
96
+ ) : (
97
97
  <>
98
98
  <span>{displayTitle}</span>
99
99
  <span className="ml-1">{displayIcon}</span>
@@ -116,7 +116,7 @@ export function GradientButton({
116
116
  return (
117
117
  <div className={`flex flex-col sm:flex-row gap-3 ${getAlignmentClass()}`}>
118
118
  {onClick ? (
119
- // 点击模式
119
+ // for click
120
120
  <Button
121
121
  size="lg"
122
122
  className={buttonClassName}
@@ -126,7 +126,7 @@ export function GradientButton({
126
126
  {buttonContent}
127
127
  </Button>
128
128
  ) : (
129
- // 跳转模式
129
+ // for Link
130
130
  <Button
131
131
  asChild
132
132
  size="lg"
package/src/main/index.ts CHANGED
@@ -13,4 +13,5 @@ export * from './go-to-top';
13
13
  export * from './loading';
14
14
  export * from './nprogress-bar';
15
15
  export * from './price-plan';
16
- export * from './ads-alert-dialog';
16
+ export * from './ads-alert-dialog';
17
+ export * from './x-button'
@@ -0,0 +1,199 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, ReactNode } from 'react'
4
+ import { globalLucideIcons as icons } from '@base-ui/components/global-icon'
5
+
6
+ // base button config
7
+ interface BaseButtonConfig {
8
+ icon: ReactNode
9
+ text: string
10
+ onClick: () => void | Promise<void>
11
+ disabled?: boolean
12
+ }
13
+
14
+ // menu item config
15
+ interface MenuItemConfig extends BaseButtonConfig {
16
+ tag?: {
17
+ text: string
18
+ color?: string
19
+ }
20
+ }
21
+
22
+ // single button config
23
+ interface SingleButtonProps {
24
+ type: 'single'
25
+ button: BaseButtonConfig
26
+ loadingText?: string
27
+ minWidth?: string
28
+ className?: string
29
+ }
30
+
31
+ // split button config
32
+ interface SplitButtonProps {
33
+ type: 'split'
34
+ mainButton: BaseButtonConfig
35
+ menuItems: MenuItemConfig[]
36
+ loadingText?: string
37
+ menuWidth?: string
38
+ className?: string
39
+ mainButtonClassName?: string
40
+ dropdownButtonClassName?: string
41
+ }
42
+
43
+ type xButtonProps = SingleButtonProps | SplitButtonProps
44
+
45
+ export const XButtonIcons = {
46
+ copy: <icons.Copy className="w-5 h-5 mr-1" />,
47
+ checkCheck: <icons.CheckCheck className="w-5 h-5 mr-1" />,
48
+ globe: <icons.Languages className="w-5 h-5 mr-1" />,
49
+ loader: <icons.Loader2 className="w-5 h-5 mr-1 animate-spin" />,
50
+ download: <icons.Download className="w-5 h-5 mr-1" />,
51
+ upload: <icons.ImageUp className="w-5 h-5 mr-1" />,
52
+ share: <icons.Share className="w-5 h-5 mr-1" />,
53
+ edit: <icons.Pencil className="w-5 h-5 mr-1" />,
54
+ }
55
+
56
+ export function XButton(props: xButtonProps) {
57
+ const [isLoading, setIsLoading] = useState(false)
58
+ const [menuOpen, setMenuOpen] = useState(false)
59
+ const menuRef = useRef<HTMLDivElement>(null)
60
+
61
+ // click outside to close menu
62
+ useEffect(() => {
63
+ if (props.type === 'split') {
64
+ const handleClickOutside = (event: MouseEvent) => {
65
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
66
+ setMenuOpen(false)
67
+ }
68
+ }
69
+
70
+ if (menuOpen) {
71
+ document.addEventListener('mousedown', handleClickOutside)
72
+ }
73
+
74
+ return () => {
75
+ document.removeEventListener('mousedown', handleClickOutside)
76
+ }
77
+ }
78
+ }, [menuOpen, props.type])
79
+
80
+ // handle button click
81
+ const handleButtonClick = async (onClick: () => void | Promise<void>) => {
82
+ if (isLoading) return
83
+
84
+ setIsLoading(true)
85
+ try {
86
+ await onClick()
87
+ } catch (error) {
88
+ console.error('Button click error:', error)
89
+ } finally {
90
+ setIsLoading(false)
91
+ }
92
+ }
93
+
94
+ // base style class
95
+ const baseButtonClass = "flex items-center justify-center px-4 py-2 bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-white text-sm font-semibold transition-colors hover:bg-neutral-300 dark:hover:bg-neutral-700"
96
+ const disabledClass = "opacity-60 cursor-not-allowed"
97
+
98
+ if (props.type === 'single') {
99
+ const { button, loadingText, minWidth = 'min-w-[110px]', className = '' } = props
100
+ const isDisabled = button.disabled || isLoading
101
+ // loadingText: props.loadingText > button.text > 'Loading...'
102
+ const actualLoadingText = loadingText || button.text?.trim() || 'Loading...'
103
+
104
+ return (
105
+ <button
106
+ onClick={() => handleButtonClick(button.onClick)}
107
+ disabled={isDisabled}
108
+ className={`${minWidth} ${baseButtonClass} rounded-full ${isDisabled ? disabledClass : ''} ${className}`}
109
+ title={button.text}
110
+ >
111
+ {isLoading ? (
112
+ <>
113
+ <icons.Loader2 className="w-5 h-5 mr-1 animate-spin" />
114
+ <span>{actualLoadingText}</span>
115
+ </>
116
+ ) : (
117
+ <>
118
+ {button.icon}
119
+ <span>{button.text}</span>
120
+ </>
121
+ )}
122
+ </button>
123
+ )
124
+ }
125
+
126
+ // Split button
127
+ const { mainButton, menuItems, loadingText, menuWidth = 'w-40', className = '', mainButtonClassName = '', dropdownButtonClassName = '' } = props
128
+ const isMainDisabled = mainButton.disabled || isLoading
129
+ // loadingText 优先级:props.loadingText > mainButton.text > 'Loading...'
130
+ const actualLoadingText = loadingText || mainButton.text?.trim() || 'Loading...'
131
+
132
+ return (
133
+ <div className={`relative flex bg-neutral-200 dark:bg-neutral-800 rounded-full ${className}`}>
134
+ {/* left main button */}
135
+ <button
136
+ onClick={() => handleButtonClick(mainButton.onClick)}
137
+ disabled={isMainDisabled}
138
+ className={`flex-1 ${baseButtonClass} rounded-l-full ${isMainDisabled ? disabledClass : ''} ${mainButtonClassName}`}
139
+ onMouseDown={e => { if (e.button === 2) e.preventDefault() }}
140
+ style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
141
+ >
142
+ {isLoading ? (
143
+ <>
144
+ <icons.Loader2 className="w-5 h-5 mr-1 animate-spin" />
145
+ <span>{actualLoadingText}</span>
146
+ </>
147
+ ) : (
148
+ <>
149
+ {mainButton.icon}
150
+ <span>{mainButton.text}</span>
151
+ </>
152
+ )}
153
+ </button>
154
+
155
+ {/* right dropdown button */}
156
+ <span
157
+ className={`flex items-center justify-center w-10 py-2 cursor-pointer transition hover:bg-neutral-300 dark:hover:bg-neutral-700 rounded-r-full ${dropdownButtonClassName}`}
158
+ onClick={e => { e.stopPropagation(); setMenuOpen(v => !v) }}
159
+ tabIndex={0}
160
+ style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
161
+ >
162
+ <icons.ChevronDown className="w-6 h-6" />
163
+ </span>
164
+
165
+ {/* dropdown menu */}
166
+ {menuOpen && (
167
+ <div
168
+ ref={menuRef}
169
+ className={`absolute right-0 top-full ${menuWidth} bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white text-sm rounded-xl shadow-lg z-50 border border-neutral-200 dark:border-neutral-700 overflow-hidden animate-fade-in`}
170
+ >
171
+ {menuItems.map((item, index) => (
172
+ <button
173
+ key={index}
174
+ onClick={() => {
175
+ handleButtonClick(item.onClick)
176
+ setMenuOpen(false)
177
+ }}
178
+ disabled={item.disabled}
179
+ className={`flex items-center w-full px-4 py-3 transition hover:bg-neutral-300 dark:hover:bg-neutral-600 text-left relative ${item.disabled ? disabledClass : ''}`}
180
+ >
181
+ <span className="flex items-center">
182
+ {item.icon}
183
+ <span>{item.text}</span>
184
+ </span>
185
+ {item.tag && (
186
+ <span
187
+ className="absolute right-3 top-1 text-[10px] font-semibold"
188
+ style={{ color: item.tag.color || '#A855F7', pointerEvents: 'none' }}
189
+ >
190
+ {item.tag.text}
191
+ </span>
192
+ )}
193
+ </button>
194
+ ))}
195
+ </div>
196
+ )}
197
+ </div>
198
+ )
199
+ }