@_tc/template-core 0.1.15 → 0.1.16
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/.skills/tc-component-usage-skills/reference/component-api.md +7 -1
- package/fe/packages/ui/react/components/Button/Button.js +3 -2
- package/fe/packages/ui/react/components/Card/Card.d.ts +5 -0
- package/fe/packages/ui/react/components/Card/Card.js +6 -2
- package/fe/packages/ui/react/components/Date/Date.js +3 -3
- package/fe/packages/ui/react/components/Drawer/Drawer.js +10 -1
- package/fe/packages/ui/react/components/Dropdown/Dropdown.js +44 -4
- package/fe/packages/ui/react/components/ImagePreview/ImagePreview.js +14 -12
- package/fe/packages/ui/react/components/ImagePreview/PreviewImage.js +1 -1
- package/fe/packages/ui/react/components/Input/Input.js +2 -2
- package/fe/packages/ui/react/components/InputNumber/InputNumber.js +2 -2
- package/fe/packages/ui/react/components/Loading/Loading.d.ts +16 -0
- package/fe/packages/ui/react/components/Loading/Loading.js +12 -4
- package/fe/packages/ui/react/components/Menu/Menu.js +9 -2
- package/fe/packages/ui/react/components/Menu/MenuItem.js +4 -4
- package/fe/packages/ui/react/components/Menu/SubMenu.js +4 -4
- package/fe/packages/ui/react/components/Message/Message.js +5 -5
- package/fe/packages/ui/react/components/Modal/Modal.js +1 -1
- package/fe/packages/ui/react/components/Notification/Notification.js +5 -5
- package/fe/packages/ui/react/components/Pagination/Pagination.js +18 -10
- package/fe/packages/ui/react/components/Popup/Popup.js +1 -1
- package/fe/packages/ui/react/components/Select/Select.js +101 -4
- package/fe/packages/ui/react/components/Textarea/Textarea.js +3 -3
- package/fe/packages/ui/react/components/Tooltip/Tooltip.js +1 -1
- package/fe/packages/ui/react/components/Upload/ImageUpload.js +2 -2
- package/fe/packages/ui/react/components/Upload/Upload.js +2 -1
- package/fe/packages/ui/react/index.css +66 -9
- package/package.json +1 -1
|
@@ -725,10 +725,16 @@ interface NotificationProps {
|
|
|
725
725
|
interface LoadingProps {
|
|
726
726
|
size?: 'sm' | 'md' | 'lg'
|
|
727
727
|
text?: ReactNode
|
|
728
|
+
className?: string
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
interface LoadingIndicatorProps {
|
|
732
|
+
size?: 'sm' | 'md' | 'lg'
|
|
733
|
+
className?: string
|
|
728
734
|
}
|
|
729
735
|
```
|
|
730
736
|
|
|
731
|
-
|
|
737
|
+
`Loading` renders the shared spinner with optional text. `LoadingIndicator` is the icon-only spinner used by compact surfaces such as `Button.loading`.
|
|
732
738
|
|
|
733
739
|
---
|
|
734
740
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from '../../lib/utils';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
|
+
import { LoadingIndicator } from '../Loading';
|
|
4
5
|
const Button = forwardRef((props, ref) => {
|
|
5
6
|
const { className, size = 'md', state, disabled: disabledProp, loading: loadingProp, leftIcon, rightIcon, noUnderline, children, } = props;
|
|
6
7
|
const variant = props.variant ?? 'default';
|
|
@@ -46,7 +47,7 @@ const Button = forwardRef((props, ref) => {
|
|
|
46
47
|
md: 'h-4 w-4',
|
|
47
48
|
lg: 'h-5 w-5',
|
|
48
49
|
};
|
|
49
|
-
const loadingIcon = loading ? (_jsx("span", { className: cn('flex items-center', children && 'mr-2'), children:
|
|
50
|
+
const loadingIcon = loading ? (_jsx("span", { className: cn('flex items-center', children && 'mr-2'), children: _jsx(LoadingIndicator, { size: "sm", className: cn('text-current', spinnerSize[size]) }) }, "loading")) : null;
|
|
50
51
|
// 展示内容
|
|
51
52
|
const contents = [
|
|
52
53
|
loadingIcon,
|
|
@@ -4,6 +4,11 @@ export type CardProps = {
|
|
|
4
4
|
footer?: ReactNode;
|
|
5
5
|
children?: ReactNode;
|
|
6
6
|
className?: string;
|
|
7
|
+
/**
|
|
8
|
+
* 是否启用 hover 悬浮阴影
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
hoverable?: boolean;
|
|
7
12
|
};
|
|
8
13
|
declare const Card: import("react").ForwardRefExoticComponent<CardProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
9
14
|
export { Card };
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from '../../lib/utils';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
|
-
const Card = forwardRef(({ header, footer, children, className }, ref) => {
|
|
5
|
-
return (_jsxs("div", { ref: ref, className: cn('tc-ui-card rounded-
|
|
4
|
+
const Card = forwardRef(({ header, footer, children, className, hoverable = true }, ref) => {
|
|
5
|
+
return (_jsxs("div", { ref: ref, className: cn('tc-ui-card rounded-xl border border-border/60 bg-card text-card-foreground', 'shadow-[inset_0_1px_0_hsl(var(--foreground)/0.04)]', hoverable && [
|
|
6
|
+
'transition-[transform,box-shadow,border-color] duration-200 ease-out',
|
|
7
|
+
'hover:-translate-y-0.5 hover:border-border/80',
|
|
8
|
+
'hover:shadow-[var(--shadow-panel),inset_0_1px_0_hsl(var(--foreground)/0.06)]',
|
|
9
|
+
], className), children: [header && (_jsx("div", { className: "tc-ui-card-header border-b border-border/50 px-4 py-3 text-sm font-medium", children: header })), _jsx("div", { className: "tc-ui-card-body px-4 py-4", children: children }), footer && (_jsx("div", { className: "tc-ui-card-footer border-t border-border/50 px-4 py-3", children: footer }))] }));
|
|
6
10
|
});
|
|
7
11
|
Card.displayName = 'Card';
|
|
8
12
|
export { Card };
|
|
@@ -187,10 +187,10 @@ const DatePicker = forwardRef((props, ref) => {
|
|
|
187
187
|
}
|
|
188
188
|
};
|
|
189
189
|
const showClearIcon = allowClear && selectedDate && !disabled && isHover;
|
|
190
|
-
return (_jsx("div", { ref: ref, className: cn("tc-ui-date-picker relative w-full", mode === ModeType.Range ? "min-w-[255px]" : "", className), children: _jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("div", { className: cn("flex items-center w-full rounded-lg px-
|
|
191
|
-
"cursor-not-allowed bg-muted border-border text-muted-foreground", !disabled && !
|
|
190
|
+
return (_jsx("div", { ref: ref, className: cn("tc-ui-date-picker relative w-full", mode === ModeType.Range ? "min-w-[255px]" : "", className), children: _jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("div", { className: cn("flex h-10 items-center w-full rounded-lg px-3 py-0 text-sm", "border transition-colors duration-200", "bg-background cursor-pointer", disabled &&
|
|
191
|
+
"cursor-not-allowed bg-muted border-border text-muted-foreground", !disabled && !isOpen && "border-border", !disabled && isOpen && "border-theme ring-2 ring-theme/25", !disabled && "focus-within:border-theme focus-within:ring-2 focus-within:ring-theme/25"), onClick: () => !disabled && setIsOpen(!isOpen), onMouseEnter: () => setIsHover(true), onMouseLeave: () => setIsHover(false), children: [_jsx(Input, { ref: inputRef, type: "text", value: inputValue, onChange: (_, e) => handleInputChange(e), placeholder: finalPlaceholder, disabled: disabled, readOnly: mode === ModeType.Range, className: "min-w-0 flex-1", inputClassName: cn("h-8 min-w-0 flex-1 rounded-none border-0 bg-transparent px-0 py-0 outline-none", "ring-0 shadow-none focus:ring-0 focus-visible:ring-0", "text-sm text-foreground placeholder:text-muted-foreground", disabled && "cursor-not-allowed", mode === ModeType.Range &&
|
|
192
192
|
!disabled &&
|
|
193
|
-
"cursor-pointer caret-transparent") }), showClearIcon ? (_jsx(Button, { variant: "text", type: "button", onClick: handleClear, title: locale.clear, "aria-label": locale.clear, className: cn("ml-2 flex items-center justify-center
|
|
193
|
+
"cursor-pointer caret-transparent") }), showClearIcon ? (_jsx(Button, { variant: "text", type: "button", onClick: handleClear, title: locale.clear, "aria-label": locale.clear, className: cn("ml-2 flex h-7 w-7 items-center justify-center rounded-md", "text-muted-foreground transition-colors hover:bg-foreground/8 hover:text-foreground", "cursor-pointer"), children: _jsx(X, { size: 12 }) })) : (_jsx(CalendarIcon, { size: 16, className: "ml-2 shrink-0 text-muted-foreground" }))] }), _jsx(Popup, { open: isOpen && !disabled, anchorRef: containerRef, contentRef: dropdownContentRef, positioning: positioning, keepMounted: true, onOpenChange: (nextOpen) => {
|
|
194
194
|
setIsOpen(nextOpen);
|
|
195
195
|
if (!nextOpen) {
|
|
196
196
|
rangeDraftRef.current = false;
|
|
@@ -16,6 +16,15 @@ const getDrawerPositionClassName = (placement) => {
|
|
|
16
16
|
};
|
|
17
17
|
return positionClassNames[placement];
|
|
18
18
|
};
|
|
19
|
+
const getDrawerRadiusClassName = (placement) => {
|
|
20
|
+
const radiusClassNames = {
|
|
21
|
+
top: 'rounded-b-2xl',
|
|
22
|
+
right: 'rounded-l-2xl',
|
|
23
|
+
bottom: 'rounded-t-2xl',
|
|
24
|
+
left: 'rounded-r-2xl',
|
|
25
|
+
};
|
|
26
|
+
return radiusClassNames[placement];
|
|
27
|
+
};
|
|
19
28
|
const getDrawerTransform = (placement, open) => {
|
|
20
29
|
if (open)
|
|
21
30
|
return 'translate3d(0, 0, 0)';
|
|
@@ -96,5 +105,5 @@ export function Drawer({ open = false, onClose, title, children, footer, classNa
|
|
|
96
105
|
};
|
|
97
106
|
return (_jsx(Overlay, { open: mounted, visible: visible, onClose: onClose, mask: mask, maskClosable: maskClosable, lockScroll: lockScroll, keyboard: keyboard, closable: closable, renderNode: renderNode, zIndex: zIndex, className: "tc-ui-drawer", style: {
|
|
98
107
|
transition: `opacity ${DRAWER_ANIMATION_DURATION}ms ${DRAWER_ANIMATION_EASING}`,
|
|
99
|
-
}, children: _jsxs("div", { className: cn('fixed bg-
|
|
108
|
+
}, children: _jsxs("div", { className: cn('fixed border border-border/70 bg-popover text-popover-foreground', 'shadow-[0_22px_70px_hsl(var(--background)/0.58),inset_0_1px_0_hsl(var(--foreground)/0.06)]', 'flex flex-col', getDrawerPositionClassName(placement), getDrawerRadiusClassName(placement), placement === 'top' || placement === 'bottom' ? 'max-h-[90vh]' : 'max-w-[90vw]', className), style: drawerStyle, onClick: handleContentClick, children: [(title || closable) && (_jsxs("div", { className: "flex items-center justify-between border-b border-border/60 px-4 py-3", children: [title && _jsx("div", { className: "text-sm font-semibold leading-5 text-foreground", children: title }), closable && (_jsx(Button, { variant: "text", type: "button", onClick: onClose, "aria-label": t('components.modal.close'), className: cn('ml-auto h-7 w-7 rounded-md p-0', 'text-muted-foreground hover:text-foreground', 'hover:bg-foreground/8', 'transition-colors', 'focus:outline-none focus:ring-2 focus:ring-ring/40 focus:ring-offset-0'), children: _jsx(X, { size: 16 }) }))] })), _jsx("div", { className: cn('flex-1 overflow-auto px-4 py-4', contentClassName), children: children }), footer && _jsx("div", { className: "border-t border-border/60 px-4 py-3", children: footer })] }) }));
|
|
100
109
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from '../../lib/utils';
|
|
3
|
-
import { Fragment, useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { cloneElement, Fragment, isValidElement, useCallback, useRef, useState } from 'react';
|
|
4
4
|
import { Button } from '../Button';
|
|
5
5
|
import { Popup } from '../Popup';
|
|
6
6
|
import { getDefaultDropdownPosition, } from '../hooks/useDropdownPositioning';
|
|
@@ -29,21 +29,61 @@ export function Dropdown({ items, children, className, placement = 'bottom-end',
|
|
|
29
29
|
const handleToggle = useCallback(() => {
|
|
30
30
|
setIsOpen((prev) => !prev);
|
|
31
31
|
}, []);
|
|
32
|
+
const handleTriggerKeyDown = useCallback((event) => {
|
|
33
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
handleToggle();
|
|
36
|
+
}
|
|
37
|
+
if (event.key === 'ArrowDown') {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
setIsOpen(true);
|
|
40
|
+
}
|
|
41
|
+
if (event.key === 'Escape') {
|
|
42
|
+
setIsOpen(false);
|
|
43
|
+
}
|
|
44
|
+
}, [handleToggle]);
|
|
32
45
|
const handleItemClick = useCallback((item) => {
|
|
33
46
|
if (!isDropdownRenderItem(item)) {
|
|
34
47
|
item.onClick?.();
|
|
35
48
|
}
|
|
36
49
|
setIsOpen(false);
|
|
37
50
|
}, []);
|
|
38
|
-
|
|
51
|
+
const triggerProps = {
|
|
52
|
+
role: 'button',
|
|
53
|
+
tabIndex: 0,
|
|
54
|
+
'aria-haspopup': 'menu',
|
|
55
|
+
'aria-expanded': isOpen,
|
|
56
|
+
className: 'outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-0',
|
|
57
|
+
};
|
|
58
|
+
const trigger = isValidElement(children)
|
|
59
|
+
? cloneElement(children, {
|
|
60
|
+
...triggerProps,
|
|
61
|
+
onClick: (event) => {
|
|
62
|
+
;
|
|
63
|
+
children.props.onClick?.(event);
|
|
64
|
+
if (!event.defaultPrevented) {
|
|
65
|
+
handleToggle();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onKeyDown: (event) => {
|
|
69
|
+
;
|
|
70
|
+
children.props.onKeyDown?.(event);
|
|
71
|
+
if (!event.defaultPrevented) {
|
|
72
|
+
handleTriggerKeyDown(event);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
className: cn(children.props.className, triggerProps.className),
|
|
76
|
+
})
|
|
77
|
+
: (_jsx("span", { ...triggerProps, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, children: children }));
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, className: cn('tc-ui-dropdown relative inline-block', className), children: trigger }), _jsx(Popup, { open: isOpen, anchorRef: triggerRef, placement: placement, onOpenChange: setIsOpen, container: getPopupContainer?.(), strategy: getDropdownPosition, className: cn('min-w-24 overflow-hidden border-border/70 bg-popover p-1 shadow-[0_22px_70px_hsl(var(--background)/0.58),inset_0_1px_0_hsl(var(--foreground)/0.06)]'), children: items.map((item, index) => {
|
|
39
79
|
const onClick = () => handleItemClick(item);
|
|
40
80
|
const close = () => setIsOpen(false);
|
|
41
81
|
const key = isDropdownRenderItem(item) ? index : item.key;
|
|
42
82
|
if (isDropdownRenderItem(item)) {
|
|
43
83
|
return _jsx(Fragment, { children: item.render(item, { onClick, close }) }, key);
|
|
44
84
|
}
|
|
45
|
-
return (_jsx(Button, { type: "button", variant: "text", state: item.danger ? 'danger' : undefined, onClick: onClick, className: cn('flex h-
|
|
85
|
+
return (_jsx(Button, { type: "button", variant: "text", state: item.danger ? 'danger' : undefined, onClick: onClick, className: cn('flex h-9 w-full items-center justify-start gap-2 rounded-md px-3 text-left text-sm transition-colors', item.danger
|
|
46
86
|
? 'text-destructive hover:bg-destructive/10'
|
|
47
|
-
: 'text-popover-foreground hover:bg-
|
|
87
|
+
: 'text-popover-foreground hover:bg-foreground/8'), children: _jsxs(_Fragment, { children: [item.icon && _jsx("span", { className: "flex-shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: item.icon }), _jsx("span", { children: item.label })] }) }, key));
|
|
48
88
|
}) })] }));
|
|
49
89
|
}
|
|
@@ -101,13 +101,15 @@ function PreviewCore({ images, initialIndex, onClose }) {
|
|
|
101
101
|
}, [rotation, scale, flipX, flipY, position]);
|
|
102
102
|
if (!hasImages)
|
|
103
103
|
return null;
|
|
104
|
-
const
|
|
104
|
+
const floatingIconButtonClassName = 'z-10 rounded-full border border-border/60 bg-popover text-popover-foreground shadow-[var(--shadow-floating)] transition-colors duration-200 hover:bg-muted/80 active:bg-muted focus-visible:ring-theme/30';
|
|
105
|
+
const toolbarButtonClassName = 'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-popover-foreground transition-colors duration-200 hover:bg-foreground/8 active:bg-foreground/12 focus-visible:ring-theme/30';
|
|
106
|
+
const content = (_jsxs("div", { className: "tc-ui-image-preview fixed inset-0 z-[1100] bg-background/80 text-foreground", onClick: onClose, children: [_jsx(Button, { variant: "text", type: "button", onClick: onClose, className: `absolute right-6 top-6 p-2 ${floatingIconButtonClassName}`, "aria-label": t('components.imagePreview.closePreview'), children: _jsx(X, { size: 20 }) }), images.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
105
107
|
e.stopPropagation();
|
|
106
108
|
toPrev();
|
|
107
|
-
}, className:
|
|
109
|
+
}, className: `absolute left-4 top-1/2 -translate-y-1/2 p-3 ${floatingIconButtonClassName}`, "aria-label": t('components.imagePreview.previousImage'), children: _jsx(ArrowLeft, { size: 20 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
108
110
|
e.stopPropagation();
|
|
109
111
|
toNext();
|
|
110
|
-
}, className:
|
|
112
|
+
}, className: `absolute right-4 top-1/2 -translate-y-1/2 p-3 ${floatingIconButtonClassName}`, "aria-label": t('components.imagePreview.nextImage'), children: _jsx(ArrowRight, { size: 20 }) })] })), _jsx("div", { className: "absolute inset-0 flex items-center justify-center overflow-hidden", onClick: (e) => e.stopPropagation(), onWheel: (e) => {
|
|
111
113
|
e.preventDefault();
|
|
112
114
|
if (e.deltaY < 0) {
|
|
113
115
|
zoomIn();
|
|
@@ -115,13 +117,13 @@ function PreviewCore({ images, initialIndex, onClose }) {
|
|
|
115
117
|
else {
|
|
116
118
|
zoomOut();
|
|
117
119
|
}
|
|
118
|
-
}, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, style: { cursor: isDragging ? 'grabbing' : scale > 1 ? 'grab' : 'default' }, children: _jsx("div", { className: "relative max-h-[85vh] max-w-[90vw]", children: _jsx("img", { ref: imgRef, src: currentImage, alt: "", draggable: false, className: " select-none max-h-[85vh] max-w-[90vw] absolute left-1/2 top-1/2 origin-center" }) }) }), _jsxs("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2
|
|
120
|
+
}, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, style: { cursor: isDragging ? 'grabbing' : scale > 1 ? 'grab' : 'default' }, children: _jsx("div", { className: "relative max-h-[85vh] max-w-[90vw]", children: _jsx("img", { ref: imgRef, src: currentImage, alt: "", draggable: false, className: " select-none max-h-[85vh] max-w-[90vw] absolute left-1/2 top-1/2 origin-center" }) }) }), _jsxs("div", { className: "absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-xl border border-border/60 bg-popover px-3 py-2 text-popover-foreground shadow-[var(--shadow-floating)]", children: [_jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
119
121
|
e.stopPropagation();
|
|
120
122
|
zoomOut();
|
|
121
|
-
}, className:
|
|
123
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.zoomOut'), children: _jsx(ZoomOut, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
122
124
|
e.stopPropagation();
|
|
123
125
|
zoomIn();
|
|
124
|
-
}, className:
|
|
126
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.zoomIn'), children: _jsx(ZoomIn, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
125
127
|
e.stopPropagation();
|
|
126
128
|
if (!imgRef.current)
|
|
127
129
|
return;
|
|
@@ -143,22 +145,22 @@ function PreviewCore({ images, initialIndex, onClose }) {
|
|
|
143
145
|
setScale(lastScaleBeforeNatural.current);
|
|
144
146
|
lastScaleBeforeNatural.current = null;
|
|
145
147
|
}
|
|
146
|
-
}, className:
|
|
148
|
+
}, className: toolbarButtonClassName, "aria-label": "1:1", children: "1:1" }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
147
149
|
e.stopPropagation();
|
|
148
150
|
reset();
|
|
149
|
-
}, className:
|
|
151
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.reset'), children: _jsx(RefreshCw, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
150
152
|
e.stopPropagation();
|
|
151
153
|
rotateLeft();
|
|
152
|
-
}, className:
|
|
154
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.rotateLeft'), children: _jsx(RotateCcw, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
153
155
|
e.stopPropagation();
|
|
154
156
|
rotateRight();
|
|
155
|
-
}, className:
|
|
157
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.rotateRight'), children: _jsx(RotateCw, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
156
158
|
e.stopPropagation();
|
|
157
159
|
setFlipX((prev) => !prev);
|
|
158
|
-
}, className:
|
|
160
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.flipHorizontal'), children: _jsx(FlipHorizontal, { size: 16 }) }), _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
159
161
|
e.stopPropagation();
|
|
160
162
|
setFlipY((prev) => !prev);
|
|
161
|
-
}, className:
|
|
163
|
+
}, className: toolbarButtonClassName, "aria-label": t('components.imagePreview.flipVertical'), children: _jsx(FlipVertical, { size: 16 }) })] })] }));
|
|
162
164
|
return createPortal(content, document.body);
|
|
163
165
|
}
|
|
164
166
|
export function ImagePreviewController({ open, images, initialIndex = 0, onClose }) {
|
|
@@ -18,7 +18,7 @@ export function PreviewImage(props) {
|
|
|
18
18
|
}, [thumbSize]);
|
|
19
19
|
if (!images?.length)
|
|
20
20
|
return null;
|
|
21
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: `flex gap-2 ${direction === 'vertical' ? 'flex-col' : ''}`, children: images.map((src, idx) => (_jsx(Button, { variant: "text", type: "button", className: `relative aspect-square overflow-hidden rounded border border-border ${sizeClass}`, style: sizeStyle, onClick: () => {
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: `flex gap-2 ${direction === 'vertical' ? 'flex-col' : ''}`, children: images.map((src, idx) => (_jsx(Button, { variant: "text", type: "button", className: `relative aspect-square overflow-hidden rounded-lg border border-border/60 bg-card shadow-[inset_0_1px_0_hsl(var(--foreground)/0.04)] transition-colors duration-200 hover:border-border/80 active:border-theme/60 focus-visible:ring-theme/30 ${sizeClass}`, style: sizeStyle, onClick: () => {
|
|
22
22
|
setCurrentIndex(idx);
|
|
23
23
|
setOpen(true);
|
|
24
24
|
}, "aria-label": t('components.imagePreview.previewImage', { index: idx + 1 }), children: _jsx("img", { src: src, alt: "", className: "h-full w-full object-cover" }) }, src + idx))) }), _jsx(ImagePreviewController, { ...ext, images: images, initialIndex: currentIndex, open: open, onClose: () => {
|
|
@@ -45,7 +45,7 @@ const Input = forwardRef(({ className, inputClassName, type, value: controlledVa
|
|
|
45
45
|
return undefined;
|
|
46
46
|
}, [allowClear]);
|
|
47
47
|
const showClearIcon = hasValue && clearIcon && !disabled;
|
|
48
|
-
return (_jsxs("div", { className: cn('tc-ui-input relative w-full', className), children: [_jsx("input", { type: type, ref: setRefs, ...(!isFileInput && { value }), onChange: handleChange, className: cn('flex w-full rounded-lg px-
|
|
48
|
+
return (_jsxs("div", { className: cn('tc-ui-input relative w-full', className), children: [_jsx("input", { type: type, ref: setRefs, ...(!isFileInput && { value }), onChange: handleChange, className: cn('flex h-10 w-full rounded-lg px-3 py-0 text-sm', 'border transition-colors duration-200', 'placeholder:text-muted-foreground', 'focus:outline-none', showClearIcon && 'pr-10', addonAfter && 'pr-12', disabled ? 'cursor-not-allowed bg-muted border-border text-muted-foreground' : 'bg-background', !disabled && !isFocused && 'border-border', !disabled && isFocused && 'border-theme ring-2 ring-theme/25', inputClassName), onFocus: (e) => {
|
|
49
49
|
if (!disabled) {
|
|
50
50
|
setIsFocused(true);
|
|
51
51
|
props.onFocus?.(e);
|
|
@@ -55,7 +55,7 @@ const Input = forwardRef(({ className, inputClassName, type, value: controlledVa
|
|
|
55
55
|
setIsFocused(false);
|
|
56
56
|
props.onBlur?.(e);
|
|
57
57
|
}
|
|
58
|
-
}, disabled: disabled, ...props }), showClearIcon && (_jsx("button", { type: "button", onClick: handleClear, className: cn('absolute right-3 top-1/2 -translate-y-1/2'), children: clearIcon })), addonAfter && _jsx("div", { className: "absolute right-2 top-1/2 -translate-y-1/2", children: addonAfter })] }));
|
|
58
|
+
}, disabled: disabled, ...props }), showClearIcon && (_jsx("button", { type: "button", onClick: handleClear, "aria-label": "Clear input", title: "Clear input", className: cn('absolute right-3 top-1/2 -translate-y-1/2'), children: clearIcon })), addonAfter && _jsx("div", { className: "absolute right-2 top-1/2 -translate-y-1/2", children: addonAfter })] }));
|
|
59
59
|
});
|
|
60
60
|
Input.displayName = 'Input';
|
|
61
61
|
export { Input };
|
|
@@ -105,7 +105,7 @@ const InputNumber = forwardRef(({ className, inputClassName, value: controlledVa
|
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
107
|
if (typeof allowClear === 'boolean') {
|
|
108
|
-
return (_jsx("span", { className: cn('h-
|
|
108
|
+
return (_jsx("span", { className: cn('h-6 w-6 rounded-md', 'text-muted-foreground', 'flex items-center justify-center', 'transition-colors hover:bg-foreground/8 hover:text-foreground'), children: _jsx(X, { size: 12 }) }));
|
|
109
109
|
}
|
|
110
110
|
return allowClear.clearIcon;
|
|
111
111
|
}, [allowClear]);
|
|
@@ -138,7 +138,7 @@ const InputNumber = forwardRef(({ className, inputClassName, value: controlledVa
|
|
|
138
138
|
emitChange(undefined, syntheticEvent, true);
|
|
139
139
|
inputRef.current?.focus();
|
|
140
140
|
}, [createSyntheticChangeEvent, emitChange]);
|
|
141
|
-
return (_jsxs("div", { className: cn('tc-ui-input-number relative w-full', className), children: [_jsx("input", { ref: setRefs, type: "text", value: displayValue, onChange: handleChange, inputMode: inputMode, className: cn('flex w-full rounded-lg px-
|
|
141
|
+
return (_jsxs("div", { className: cn('tc-ui-input-number relative w-full', className), children: [_jsx("input", { ref: setRefs, type: "text", value: displayValue, onChange: handleChange, inputMode: inputMode, className: cn('flex h-10 w-full rounded-lg px-3 py-0 text-sm', 'border transition-colors duration-200', 'placeholder:text-muted-foreground', 'focus:outline-none', showClearIcon && !allowStep && !addonAfter && 'pr-10', allowStep && !showClearIcon && !addonAfter && 'pr-12', allowStep && showClearIcon && !addonAfter && 'pr-20', addonAfter && !allowStep && 'pr-16', allowStep && addonAfter && 'pr-28', disabled ? 'cursor-not-allowed bg-muted border-border text-muted-foreground' : 'bg-background', !disabled && !isFocused && 'border-border', !disabled && isFocused && 'border-theme ring-2 ring-theme/25', inputClassName), onFocus: handleFocus, onBlur: handleBlur, disabled: disabled, ...props }), hasRightAccessories && (_jsxs("div", { className: "absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1", children: [showClearIcon && (_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: handleClear, "aria-label": "Clear number", title: "Clear number", className: "flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-foreground/8", children: clearIcon })), addonAfter && _jsx("div", { className: "flex h-7 items-center text-muted-foreground", children: addonAfter }), allowStep && (_jsxs("div", { className: "flex h-8 w-6 flex-col overflow-hidden rounded-md border border-border/60 bg-muted/30 text-muted-foreground", children: [_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => handleStep('up'), disabled: !canStep('up'), "aria-label": "Increase value", title: "Increase value", className: cn('flex h-4 w-full items-center justify-center transition-colors', 'border-b border-border/50', 'disabled:cursor-not-allowed disabled:text-muted-foreground/45', !disabled && canStep('up') && 'hover:bg-foreground/8 hover:text-foreground'), children: _jsx(ChevronUp, { size: 12 }) }), _jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => handleStep('down'), disabled: !canStep('down'), "aria-label": "Decrease value", title: "Decrease value", className: cn('flex h-4 w-full items-center justify-center transition-colors', 'disabled:cursor-not-allowed disabled:text-muted-foreground/45', !disabled && canStep('down') && 'hover:bg-foreground/8 hover:text-foreground'), children: _jsx(ChevronDown, { size: 12 }) })] }))] }))] }));
|
|
142
142
|
});
|
|
143
143
|
InputNumber.displayName = 'InputNumber';
|
|
144
144
|
export { InputNumber };
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
export type LoadingProps = {
|
|
3
|
+
/**
|
|
4
|
+
* 加载图标尺寸
|
|
5
|
+
* @default 'md'
|
|
6
|
+
*/
|
|
3
7
|
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
/**
|
|
9
|
+
* 加载文案
|
|
10
|
+
*/
|
|
4
11
|
text?: ReactNode;
|
|
5
12
|
className?: string;
|
|
6
13
|
};
|
|
14
|
+
export type LoadingIndicatorProps = {
|
|
15
|
+
/**
|
|
16
|
+
* 仅图标尺寸,适合被 Button 等组件复用
|
|
17
|
+
* @default 'md'
|
|
18
|
+
*/
|
|
19
|
+
size?: 'sm' | 'md' | 'lg';
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function LoadingIndicator({ size, className }: LoadingIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
7
23
|
declare const Loading: import("react").ForwardRefExoticComponent<LoadingProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
8
24
|
export { Loading };
|
|
9
25
|
//# sourceMappingURL=Loading.d.ts.map
|
|
@@ -2,12 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cn } from '../../lib/utils';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
const sizeStyles = {
|
|
5
|
-
sm: 'h-4 w-4
|
|
6
|
-
md: 'h-
|
|
7
|
-
lg: 'h-
|
|
5
|
+
sm: 'h-4 w-4',
|
|
6
|
+
md: 'h-6 w-6',
|
|
7
|
+
lg: 'h-8 w-8',
|
|
8
8
|
};
|
|
9
|
+
const textStyles = {
|
|
10
|
+
sm: 'text-xs',
|
|
11
|
+
md: 'text-xs',
|
|
12
|
+
lg: 'text-sm',
|
|
13
|
+
};
|
|
14
|
+
export function LoadingIndicator({ size = 'md', className }) {
|
|
15
|
+
return (_jsxs("svg", { className: cn('animate-spin text-theme', sizeStyles[size], className), xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }));
|
|
16
|
+
}
|
|
9
17
|
const Loading = forwardRef(({ size = 'md', text, className }, ref) => {
|
|
10
|
-
return (_jsxs("div", { ref: ref, className: cn('tc-ui-loading flex flex-col items-center justify-center gap-2', className), children: [_jsx(
|
|
18
|
+
return (_jsxs("div", { ref: ref, className: cn('tc-ui-loading inline-flex flex-col items-center justify-center gap-2 text-muted-foreground', className), children: [_jsx(LoadingIndicator, { size: size }), text && (_jsx("span", { className: cn('font-medium leading-none tracking-normal text-muted-foreground', textStyles[size]), children: text }))] }));
|
|
11
19
|
});
|
|
12
20
|
Loading.displayName = 'Loading';
|
|
13
21
|
export { Loading };
|
|
@@ -29,6 +29,13 @@ export const CollapseToggle = forwardRef(({ className }, ref) => {
|
|
|
29
29
|
}
|
|
30
30
|
return true;
|
|
31
31
|
};
|
|
32
|
+
const toggleCollapsed = () => setCollapsed(!collapsed);
|
|
33
|
+
const handleKeyDown = (event) => {
|
|
34
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
toggleCollapsed();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
32
39
|
useImperativeHandle(ref, () => ({
|
|
33
40
|
collapse: () => setCollapsed(true),
|
|
34
41
|
expand: () => setCollapsed(false),
|
|
@@ -36,14 +43,14 @@ export const CollapseToggle = forwardRef(({ className }, ref) => {
|
|
|
36
43
|
if (!shouldShowCollapseToggle()) {
|
|
37
44
|
return null;
|
|
38
45
|
}
|
|
39
|
-
return (_jsx("
|
|
46
|
+
return (_jsx("button", { type: "button", "aria-label": collapsed ? "Expand menu" : "Collapse menu", "aria-expanded": !collapsed, onClick: toggleCollapsed, onKeyDown: handleKeyDown, className: cn("mx-1 mb-1 flex h-9 cursor-pointer items-center justify-center rounded-lg", "text-muted-foreground transition-all", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-0", "hover:bg-foreground/8 hover:text-foreground", className), children: collapsed ? (_jsx(ChevronRight, { className: "h-4 w-4" })) : (_jsx(ChevronLeft, { className: "h-4 w-4" })) }));
|
|
40
47
|
});
|
|
41
48
|
CollapseToggle.displayName = "CollapseToggle";
|
|
42
49
|
function MenuInner({ children, items, className, mode, }) {
|
|
43
50
|
const collapsed = useMenuStore((s) => s.collapsed);
|
|
44
51
|
const collapsedWidth = useMenuStore((s) => s.collapsedWidth);
|
|
45
52
|
const content = items ? renderItems(items) : children;
|
|
46
|
-
return (_jsxs("nav", { role: "menu", className: cn("tc-ui-menu relative overflow-hidden rounded-xl bg-
|
|
53
|
+
return (_jsxs("nav", { role: "menu", className: cn("tc-ui-menu relative overflow-hidden rounded-xl border border-border/60 bg-card shadow-none", "shadow-[inset_0_1px_0_hsl(var(--foreground)/0.04)]", mode === "left" && "flex flex-col p-1", mode === "top" && "flex flex-row items-center p-[3px]", className), style: mode === "left" && collapsed ? { width: collapsedWidth } : undefined, children: [mode === "left" && _jsx(CollapseToggle, {}), _jsx(CollapsedContext.Provider, { value: mode === "left" && collapsed, children: _jsx("div", { className: cn(mode === "left" && "flex flex-col gap-1", mode === "top" && "flex flex-row items-center gap-1"), children: content }) })] }));
|
|
47
54
|
}
|
|
48
55
|
export const Menu = ({ mode = "left", expandTrigger = "click", expandMode = "accordion", selectedKey: controlledSelectedKey, defaultSelectedKey, onSelect, items, children, className, collapsed: controlledCollapsed, defaultCollapsed, onCollapse, collapsedWidth = 64, collapsedLabelTooltip = true, }) => {
|
|
49
56
|
const storeRef = useRef(null);
|
|
@@ -25,9 +25,9 @@ export function MenuItem({ eventKey, icon, disabled, danger, onClick, children,
|
|
|
25
25
|
collapsed &&
|
|
26
26
|
"h-10 w-full justify-center gap-0 px-0 py-2", mode === "left" &&
|
|
27
27
|
!collapsed &&
|
|
28
|
-
"min-h-10 w-full gap-3 px-3 py-2.5 text-left", isSelected && "bg-theme text-
|
|
28
|
+
"min-h-10 w-full gap-3 px-3 py-2.5 text-left", isSelected && "bg-theme/12 text-foreground", !isSelected &&
|
|
29
29
|
!danger &&
|
|
30
|
-
"text-foreground hover:bg-
|
|
31
|
-
? "bg-
|
|
32
|
-
: "text-muted-foreground group-hover:text-foreground"), children: icon })), !collapsed && (_jsx("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap", children: children })), collapsed && !icon && collapsedLabelTooltip && (_jsx("span", { className: "absolute left-0 right-0 top-0 flex h-full items-center justify-center text-[10px]", children: getFirstChar(children) }))] }), _jsx(Popup, { open: showTooltip, anchorRef: triggerRef, placement: "right-start", offset: 8, onOpenChange: setShowTooltip, className: "border-0 bg-transparent p-0 shadow-none", keepMounted: true, children: _jsx("div", { className: "whitespace-nowrap rounded-lg bg-
|
|
30
|
+
"text-foreground hover:bg-foreground/8", danger && !isSelected && "text-destructive hover:bg-destructive/10", disabled && "cursor-not-allowed opacity-50", className), style: collapsed ? style : { width, minWidth, maxWidth, ...style }, children: [icon && (_jsx("span", { className: cn("flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md transition-colors", "[&>svg]:h-4 [&>svg]:w-4", isSelected
|
|
31
|
+
? "bg-foreground/8 text-foreground"
|
|
32
|
+
: "text-muted-foreground group-hover:text-foreground"), children: icon })), !collapsed && (_jsx("span", { className: "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap", children: children })), collapsed && !icon && collapsedLabelTooltip && (_jsx("span", { className: "absolute left-0 right-0 top-0 flex h-full items-center justify-center text-[10px]", children: getFirstChar(children) }))] }), _jsx(Popup, { open: showTooltip, anchorRef: triggerRef, placement: "right-start", offset: 8, onOpenChange: setShowTooltip, className: "border-0 bg-transparent p-0 shadow-none", keepMounted: true, children: _jsx("div", { className: "whitespace-nowrap rounded-lg border border-border/60 bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-[0_12px_30px_hsl(var(--background)/0.45)]", children: children }) })] }));
|
|
33
33
|
}
|
|
@@ -69,9 +69,9 @@ function SubMenuItem({ triggerRef, onClick, icon, label, disabled, collapsed, de
|
|
|
69
69
|
collapsed &&
|
|
70
70
|
"h-10 w-full justify-center gap-0 px-0 py-2", mode === "left" &&
|
|
71
71
|
!collapsed &&
|
|
72
|
-
"min-h-10 w-full gap-3 px-3 py-2.5 text-left", descendantSelected && "bg-theme/
|
|
73
|
-
"text-foreground hover:bg-
|
|
74
|
-
? "bg-
|
|
72
|
+
"min-h-10 w-full gap-3 px-3 py-2.5 text-left", descendantSelected && "bg-theme/12 text-foreground", !descendantSelected &&
|
|
73
|
+
"text-foreground hover:bg-foreground/8", disabled && "cursor-not-allowed opacity-50"), children: [icon && (_jsx("span", { className: cn("flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md transition-colors", "[&>svg]:h-4 [&>svg]:w-4", descendantSelected
|
|
74
|
+
? "bg-foreground/8 text-foreground"
|
|
75
75
|
: "text-muted-foreground group-hover:text-foreground"), children: icon })), !collapsed && _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), collapsed && !icon && collapsedLabelTooltip && (_jsx("span", { className: "absolute left-0 right-0 top-0 flex h-full items-center justify-center text-[10px]", children: getFirstChar(label) })), !collapsed && showArrow && (_jsx(ArrowIcon, { className: "h-4 w-4 flex-shrink-0 text-muted-foreground transition-transform group-hover:text-foreground" })), _jsx("div", { ref: setPopupContentRef })] }));
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
@@ -154,7 +154,7 @@ export function SubMenu({ eventKey, label, icon, disabled, popup: forcePopup, pa
|
|
|
154
154
|
const hoverBridgePlacement = mode === "top" && level > 0 ? "horizontal" : "auto";
|
|
155
155
|
const subMenuContent = (_jsx("div", { className: cn(
|
|
156
156
|
// "min-w-[180px]",
|
|
157
|
-
"rounded-xl border border-border/
|
|
157
|
+
"rounded-xl border border-border/70 bg-popover p-2 text-popover-foreground", "shadow-[0_22px_70px_hsl(var(--background)/0.58),inset_0_1px_0_hsl(var(--foreground)/0.06)]"), children: _jsx(SubMenuLevelContext.Provider, { value: level + 1, children: _jsx("div", { className: "flex flex-col gap-1", children: children }) }) }));
|
|
158
158
|
/**
|
|
159
159
|
*
|
|
160
160
|
* submenu 生成的节点是 1 menuItem
|
|
@@ -8,19 +8,19 @@ import { DEFAULT_DURATION } from './data';
|
|
|
8
8
|
const typeConfig = {
|
|
9
9
|
success: {
|
|
10
10
|
icon: CheckCircle2,
|
|
11
|
-
color: 'text-
|
|
11
|
+
color: 'text-chart-4',
|
|
12
12
|
},
|
|
13
13
|
error: {
|
|
14
14
|
icon: XCircle,
|
|
15
|
-
color: 'text-
|
|
15
|
+
color: 'text-destructive',
|
|
16
16
|
},
|
|
17
17
|
warning: {
|
|
18
18
|
icon: AlertCircle,
|
|
19
|
-
color: 'text-
|
|
19
|
+
color: 'text-chart-5',
|
|
20
20
|
},
|
|
21
21
|
info: {
|
|
22
22
|
icon: Info,
|
|
23
|
-
color: 'text-
|
|
23
|
+
color: 'text-chart-3',
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
export function Message({ type = 'info', content, children, duration = DEFAULT_DURATION, onClose, closable = true, className, }) {
|
|
@@ -54,5 +54,5 @@ export function Message({ type = 'info', content, children, duration = DEFAULT_D
|
|
|
54
54
|
}, [duration]);
|
|
55
55
|
if (!visible)
|
|
56
56
|
return null;
|
|
57
|
-
return (_jsxs("div", { ref: messageRef, className: cn('tc-ui-message flex items-start gap-3 px-4 py-3
|
|
57
|
+
return (_jsxs("div", { ref: messageRef, className: cn('tc-ui-message flex items-start gap-3 rounded-xl px-4 py-3', 'border border-border/60 bg-popover text-popover-foreground shadow-[var(--shadow-floating)]', 'min-w-[320px] max-w-[480px]', 'transition-[opacity,transform] duration-200', 'pointer-events-auto', visible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2', className), children: [_jsx(Icon, { size: 20, className: cn('flex-shrink-0 mt-0.5', iconColor) }), _jsx("div", { className: "flex-1 text-sm text-popover-foreground", children: displayContent }), closable && (_jsx(Button, { variant: "text", type: "button", onClick: handleClose, "aria-label": t('components.message.close'), className: cn('flex-shrink-0 p-0.5 rounded-md', 'text-muted-foreground hover:text-popover-foreground', 'hover:bg-foreground/8 active:bg-foreground/12', 'transition-colors', 'focus-visible:ring-theme/30'), children: _jsx(X, { size: 16 }) }))] }));
|
|
58
58
|
}
|
|
@@ -10,5 +10,5 @@ export function Modal({ open = false, onClose, title, children, footer, classNam
|
|
|
10
10
|
const handleContentClick = (e) => {
|
|
11
11
|
e.stopPropagation();
|
|
12
12
|
};
|
|
13
|
-
return (_jsx(Overlay, { open: open, onClose: onClose, mask: mask, maskClosable: maskClosable, lockScroll: lockScroll, keyboard: keyboard, closable: closable, renderNode: renderNode, zIndex: zIndex, className: cn('tc-ui-modal flex items-center justify-center'), children: _jsxs("div", { className: cn('relative bg-
|
|
13
|
+
return (_jsx(Overlay, { open: open, onClose: onClose, mask: mask, maskClosable: maskClosable, lockScroll: lockScroll, keyboard: keyboard, closable: closable, renderNode: renderNode, zIndex: zIndex, className: cn('tc-ui-modal flex items-center justify-center'), children: _jsxs("div", { className: cn('relative rounded-2xl border border-border/70 bg-popover text-popover-foreground', 'shadow-[0_22px_70px_hsl(var(--background)/0.58),inset_0_1px_0_hsl(var(--foreground)/0.06)]', 'flex flex-col max-h-[90vh]', 'max-w-[calc(100vw-2rem)]', 'transition-all duration-200', open ? 'scale-100 opacity-100' : 'scale-95 opacity-0', centered ? 'my-auto' : 'mt-[10vh]', className), style: { width: typeof width === 'number' ? `${width}px` : width }, onClick: handleContentClick, children: [(title || closable) && (_jsxs("div", { className: "flex items-center justify-between border-b border-border/60 px-4 py-3", children: [title && _jsx("div", { className: "text-sm font-semibold leading-5 text-foreground", children: title }), closable && (_jsx(Button, { variant: "text", type: "button", onClick: onClose, "aria-label": t('components.modal.close'), className: cn('ml-auto h-7 w-7 rounded-md p-0', 'text-muted-foreground hover:text-foreground', 'hover:bg-foreground/8', 'transition-colors', 'focus:outline-none focus:ring-2 focus:ring-ring/40 focus:ring-offset-0'), children: _jsx(X, { size: 16 }) }))] })), _jsx("div", { className: cn('flex-1 overflow-auto px-4 py-4', contentClassName), children: children }), footer && _jsx("div", { className: "border-t border-border/60 px-4 py-3", children: footer })] }) }));
|
|
14
14
|
}
|
|
@@ -3,10 +3,10 @@ import { cn } from '../../lib/utils';
|
|
|
3
3
|
import { X } from 'lucide-react';
|
|
4
4
|
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
|
5
5
|
const typeStyles = {
|
|
6
|
-
info: '
|
|
7
|
-
success: '
|
|
8
|
-
warning: '
|
|
9
|
-
error: '
|
|
6
|
+
info: 'text-chart-3',
|
|
7
|
+
success: 'text-chart-4',
|
|
8
|
+
warning: 'text-chart-5',
|
|
9
|
+
error: 'text-destructive',
|
|
10
10
|
};
|
|
11
11
|
const Notification = forwardRef(({ type = 'info', title, message, duration, closable = true, onClose, className }, ref) => {
|
|
12
12
|
const [visible, setVisible] = useState(true);
|
|
@@ -23,7 +23,7 @@ const Notification = forwardRef(({ type = 'info', title, message, duration, clos
|
|
|
23
23
|
}, [duration, handleClose]);
|
|
24
24
|
if (!visible)
|
|
25
25
|
return null;
|
|
26
|
-
return (_jsxs("div", { ref: ref, role: "alert", className: cn('tc-ui-notification flex items-start gap-3 rounded-
|
|
26
|
+
return (_jsxs("div", { ref: ref, role: "alert", className: cn('tc-ui-notification flex items-start gap-3 rounded-xl border border-border/60 bg-popover p-4 text-popover-foreground shadow-[var(--shadow-floating)]', className), children: [_jsx("span", { className: cn('mt-1 h-2 w-2 shrink-0 rounded-full bg-current', typeStyles[type]), "aria-hidden": "true" }), _jsxs("div", { className: "flex-1 min-w-0", children: [title && _jsx("div", { className: "font-medium text-sm text-popover-foreground", children: title }), message && _jsx("div", { className: "text-sm text-muted-foreground mt-1", children: message })] }), closable && (_jsx("button", { type: "button", onClick: handleClose, className: "shrink-0 rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-foreground/8 hover:text-popover-foreground active:bg-foreground/12 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-theme/30", "aria-label": "Close notification", children: _jsx(X, { className: "h-4 w-4" }) }))] }));
|
|
27
27
|
});
|
|
28
28
|
Notification.displayName = 'Notification';
|
|
29
29
|
export { Notification };
|
|
@@ -5,6 +5,9 @@ import { Button } from '../Button';
|
|
|
5
5
|
import { Select } from '../Select';
|
|
6
6
|
export function Pagination({ current, pageSize, total, onChange, onPageSizeChange, pageSizeOptions = [10, 20, 50, 100], className, }) {
|
|
7
7
|
const totalPages = Math.ceil(total / pageSize);
|
|
8
|
+
const releaseFocus = (event) => {
|
|
9
|
+
event.currentTarget.blur();
|
|
10
|
+
};
|
|
8
11
|
// 生成页码数组
|
|
9
12
|
const getPageNumbers = () => {
|
|
10
13
|
const pages = [];
|
|
@@ -46,17 +49,20 @@ export function Pagination({ current, pageSize, total, onChange, onPageSizeChang
|
|
|
46
49
|
}
|
|
47
50
|
return pages;
|
|
48
51
|
};
|
|
49
|
-
const handlePrevious = () => {
|
|
52
|
+
const handlePrevious = (event) => {
|
|
53
|
+
releaseFocus(event);
|
|
50
54
|
if (current > 1) {
|
|
51
55
|
onChange(current - 1);
|
|
52
56
|
}
|
|
53
57
|
};
|
|
54
|
-
const handleNext = () => {
|
|
58
|
+
const handleNext = (event) => {
|
|
59
|
+
releaseFocus(event);
|
|
55
60
|
if (current < totalPages) {
|
|
56
61
|
onChange(current + 1);
|
|
57
62
|
}
|
|
58
63
|
};
|
|
59
|
-
const handlePageClick = (page) => {
|
|
64
|
+
const handlePageClick = (page, event) => {
|
|
65
|
+
releaseFocus(event);
|
|
60
66
|
if (typeof page === 'number' && page !== current) {
|
|
61
67
|
onChange(page);
|
|
62
68
|
}
|
|
@@ -72,16 +78,18 @@ export function Pagination({ current, pageSize, total, onChange, onPageSizeChang
|
|
|
72
78
|
};
|
|
73
79
|
if (totalPages === 0)
|
|
74
80
|
return null;
|
|
75
|
-
return (_jsxs("div", { className: cn('tc-ui-pagination flex items-center gap-4', className), children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Total ", total] }), _jsxs("div", { className: "flex-1
|
|
81
|
+
return (_jsxs("div", { className: cn('tc-ui-pagination flex items-center gap-4', className), children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Total ", total] }), _jsxs("div", { className: "flex flex-1 items-center justify-center gap-1.5", children: [_jsx(Button, { onClick: handlePrevious, disabled: current === 1, size: "sm", className: cn('inline-flex items-center justify-center', 'h-8 w-8 rounded-md px-0', 'text-sm font-medium', 'border border-border/70 bg-background', 'transition-colors duration-150', 'focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-0', current === 1
|
|
82
|
+
? 'cursor-not-allowed text-muted-foreground/45'
|
|
83
|
+
: 'cursor-pointer text-muted-foreground hover:bg-foreground/8 hover:text-foreground'), children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), getPageNumbers().map((page, index) => {
|
|
76
84
|
if (page === '...') {
|
|
77
85
|
return (_jsx("span", { className: "px-2 text-muted-foreground", children: "..." }, `ellipsis-${index}`));
|
|
78
86
|
}
|
|
79
|
-
return (_jsx(Button, { onClick: () => handlePageClick(page), size: "sm", className: cn('inline-flex items-center justify-center', 'h-8 min-w-
|
|
80
|
-
? 'bg-theme text-
|
|
81
|
-
: 'bg-background border-border hover:bg-
|
|
82
|
-
}), _jsx(Button, { onClick: handleNext, size: "sm", disabled: current === totalPages, className: cn('inline-flex items-center justify-center', 'h-8
|
|
83
|
-
? 'cursor-not-allowed
|
|
84
|
-
: '
|
|
87
|
+
return (_jsx(Button, { onClick: (event) => handlePageClick(page, event), size: "sm", "aria-current": page === current ? 'page' : undefined, className: cn('inline-flex items-center justify-center', 'h-8 min-w-8 rounded-md px-2.5', 'text-sm font-medium', 'border transition-colors duration-150', 'focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-0', page === current
|
|
88
|
+
? 'border-theme/45 bg-theme/12 text-foreground cursor-default'
|
|
89
|
+
: 'bg-background border-border/70 text-muted-foreground hover:bg-foreground/8 hover:text-foreground cursor-pointer'), children: page }, page));
|
|
90
|
+
}), _jsx(Button, { onClick: handleNext, size: "sm", disabled: current === totalPages, className: cn('inline-flex items-center justify-center', 'h-8 w-8 rounded-md px-0', 'text-sm font-medium', 'border border-border/70 bg-background', 'transition-colors duration-150', 'focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:ring-offset-0', current === totalPages
|
|
91
|
+
? 'cursor-not-allowed text-muted-foreground/45'
|
|
92
|
+
: 'cursor-pointer text-muted-foreground hover:bg-foreground/8 hover:text-foreground'), children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] }), onPageSizeChange && (_jsx("div", { className: "flex items-center gap-2", children: _jsx(Select, { value: pageSize, onChange: handlePageSizeChange, options: pageSizeOptions.map((size) => ({
|
|
85
93
|
label: `${size}`,
|
|
86
94
|
value: size,
|
|
87
95
|
})), className: "w-20 [&>div]:bg-muted [&>div]:border-border [&>div]:shadow-sm [&>div]:py-2 [&>div]:px-3 [&>div]:h-8", clearable: false, searchable: false }) }))] }));
|
|
@@ -3,7 +3,7 @@ import { cn } from "../../lib/utils";
|
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { getDefaultDropdownPosition, useDropdownPositioning, } from "../hooks/useDropdownPositioning";
|
|
6
|
-
const DEFAULT_POPUP_CLASSNAME = cn("fixed z-[
|
|
6
|
+
const DEFAULT_POPUP_CLASSNAME = cn("fixed z-[1000]", "rounded-xl border border-border/60 bg-popover text-popover-foreground", "shadow-[var(--shadow-floating)]", "animate-in fade-in-0 zoom-in-95");
|
|
7
7
|
const canUseDocument = () => typeof document !== "undefined";
|
|
8
8
|
export function Popup({ open, anchorRef, children, className, style, placement = "bottom-end", offset = 8, keepMounted = false, closeOnClickOutside = true, closeOnEscape = true, matchAnchorWidth = false, position, positioning, strategy = getDefaultDropdownPosition, contentRef, container, positionMode = "fixed", hoverBridge = 0, hoverBridgePlacement = "auto", onOpenChange, }) {
|
|
9
9
|
// 默认自己保存弹窗 DOM;如果外部传了 contentRef,就用外部的。
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useI18n } from '../../i18n';
|
|
3
3
|
import { cn } from '../../lib/utils';
|
|
4
4
|
import { ChevronUp, X } from 'lucide-react';
|
|
5
|
-
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { Button } from '../Button';
|
|
7
7
|
import { Input } from '../Input';
|
|
8
8
|
import { Popup } from '../Popup';
|
|
@@ -14,11 +14,14 @@ const Select = forwardRef(({ options, value: controlledValue, defaultValue, plac
|
|
|
14
14
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
15
15
|
const [isHover, setIsHover] = useState(false);
|
|
16
16
|
const [searchTerm, setSearchTerm] = useState('');
|
|
17
|
+
const [activeOptionIndex, setActiveOptionIndex] = useState(0);
|
|
17
18
|
const rootRef = useRef(null);
|
|
18
19
|
const selectRef = useRef(null);
|
|
19
20
|
const dropdownRef = useRef(null);
|
|
20
21
|
const inputRef = useRef(null);
|
|
21
22
|
const popupContainerRef = useRef(null);
|
|
23
|
+
const reactId = useId();
|
|
24
|
+
const listboxId = `${reactId}-listbox`;
|
|
22
25
|
const isParentPopupContainer = getPopupContainer === 'parent';
|
|
23
26
|
popupContainerRef.current = isParentPopupContainer ? (rootRef.current?.parentElement ?? null) : null;
|
|
24
27
|
useEffect(() => {
|
|
@@ -59,6 +62,15 @@ const Select = forwardRef(({ options, value: controlledValue, defaultValue, plac
|
|
|
59
62
|
return searchText.toLowerCase().includes(lower);
|
|
60
63
|
});
|
|
61
64
|
}, [options, searchTerm, searchable]);
|
|
65
|
+
const activeOption = filteredOptions[activeOptionIndex];
|
|
66
|
+
const activeOptionId = activeOption ? `${reactId}-option-${activeOptionIndex}` : undefined;
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setActiveOptionIndex((current) => {
|
|
69
|
+
if (filteredOptions.length === 0)
|
|
70
|
+
return 0;
|
|
71
|
+
return Math.min(current, filteredOptions.length - 1);
|
|
72
|
+
});
|
|
73
|
+
}, [filteredOptions.length]);
|
|
62
74
|
const positioning = useDropdownPositioning({
|
|
63
75
|
open: isOpen,
|
|
64
76
|
anchorRef: selectRef,
|
|
@@ -92,9 +104,21 @@ const Select = forwardRef(({ options, value: controlledValue, defaultValue, plac
|
|
|
92
104
|
if (next && searchable) {
|
|
93
105
|
setSearchTerm('');
|
|
94
106
|
}
|
|
107
|
+
if (next) {
|
|
108
|
+
setActiveOptionIndex(Math.max(options.findIndex((opt) => opt.value === value), 0));
|
|
109
|
+
}
|
|
95
110
|
return next;
|
|
96
111
|
});
|
|
97
|
-
}, [disabled, searchable]);
|
|
112
|
+
}, [disabled, options, searchable, value]);
|
|
113
|
+
const openSelect = useCallback(() => {
|
|
114
|
+
if (disabled)
|
|
115
|
+
return;
|
|
116
|
+
if (searchable) {
|
|
117
|
+
setSearchTerm('');
|
|
118
|
+
}
|
|
119
|
+
setActiveOptionIndex(Math.max(options.findIndex((opt) => opt.value === value), 0));
|
|
120
|
+
setIsOpen(true);
|
|
121
|
+
}, [disabled, options, searchable, value]);
|
|
98
122
|
const handleSelect = useCallback((optionValue) => {
|
|
99
123
|
if (!isControlled) {
|
|
100
124
|
setInternalValue(optionValue);
|
|
@@ -112,11 +136,84 @@ const Select = forwardRef(({ options, value: controlledValue, defaultValue, plac
|
|
|
112
136
|
}, [disabled, onChange]);
|
|
113
137
|
const handleSearchChange = useCallback((event) => {
|
|
114
138
|
setSearchTerm(event.target.value);
|
|
139
|
+
setActiveOptionIndex(0);
|
|
115
140
|
}, []);
|
|
141
|
+
const moveActiveOption = useCallback((direction) => {
|
|
142
|
+
if (filteredOptions.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
setActiveOptionIndex((current) => {
|
|
145
|
+
const nextIndex = current + direction;
|
|
146
|
+
if (nextIndex < 0)
|
|
147
|
+
return filteredOptions.length - 1;
|
|
148
|
+
if (nextIndex >= filteredOptions.length)
|
|
149
|
+
return 0;
|
|
150
|
+
return nextIndex;
|
|
151
|
+
});
|
|
152
|
+
}, [filteredOptions.length]);
|
|
153
|
+
const selectActiveOption = useCallback(() => {
|
|
154
|
+
const option = filteredOptions[activeOptionIndex];
|
|
155
|
+
if (option) {
|
|
156
|
+
handleSelect(option.value);
|
|
157
|
+
}
|
|
158
|
+
}, [activeOptionIndex, filteredOptions, handleSelect]);
|
|
159
|
+
const handleKeyDown = useCallback((event) => {
|
|
160
|
+
if (disabled)
|
|
161
|
+
return;
|
|
162
|
+
switch (event.key) {
|
|
163
|
+
case 'ArrowDown':
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
if (!isOpen) {
|
|
166
|
+
openSelect();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
moveActiveOption(1);
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
case 'ArrowUp':
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
if (!isOpen) {
|
|
175
|
+
openSelect();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
moveActiveOption(-1);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case 'Enter':
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
if (!isOpen) {
|
|
184
|
+
openSelect();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
selectActiveOption();
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case ' ':
|
|
191
|
+
if (event.currentTarget !== inputRef.current) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
if (!isOpen) {
|
|
194
|
+
openSelect();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case 'Escape':
|
|
199
|
+
if (isOpen) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
setIsOpen(false);
|
|
202
|
+
selectRef.current?.focus();
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}, [disabled, isOpen, moveActiveOption, openSelect, selectActiveOption]);
|
|
116
209
|
const popupContainer = getPopupContainer === 'parent' ? (rootRef.current?.parentElement ?? undefined) : getPopupContainer?.();
|
|
117
|
-
return (_jsxs("div", { ref: rootRef, className: cn('tc-ui-select relative w-full', className), children: [_jsxs("div", { ref: setRefs, onClick: handleToggle, onMouseEnter: () => setIsHover(true), onMouseLeave: () => setIsHover(false), className: cn('flex w-full items-center justify-between rounded-lg px-
|
|
210
|
+
return (_jsxs("div", { ref: rootRef, className: cn('tc-ui-select relative w-full', className), children: [_jsxs("div", { ref: setRefs, onClick: handleToggle, onKeyDown: handleKeyDown, onMouseEnter: () => setIsHover(true), onMouseLeave: () => setIsHover(false), role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-controls": listboxId, "aria-activedescendant": isOpen ? activeOptionId : undefined, tabIndex: disabled ? -1 : 0, className: cn('flex h-10 w-full items-center justify-between rounded-lg px-3 py-0 text-sm', 'border transition-colors duration-200', 'focus:outline-none focus-visible:border-theme focus-visible:ring-2 focus-visible:ring-theme/25', disabled ? 'cursor-not-allowed bg-muted border-border' : 'cursor-pointer bg-background', !disabled && isOpen && 'border-theme ring-2 ring-theme/25', !disabled && !isOpen && 'border-border'), children: [searchable && isOpen ? (_jsx(Input, { ref: inputRef, value: searchTerm, onChange: (_, e) => handleSearchChange(e), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
handleKeyDown(e);
|
|
213
|
+
}, placeholder: typeof displayText === 'string' ? displayText : placeholder, className: "min-w-0 flex-1", inputClassName: cn('h-5 min-w-0 flex-1 rounded-none border-0 bg-transparent px-0 py-0 text-left leading-5 outline-none ring-0', 'placeholder:text-muted-foreground', disabled && 'text-muted-foreground cursor-not-allowed'), role: "searchbox", "aria-controls": listboxId, "aria-activedescendant": isOpen ? activeOptionId : undefined, disabled: disabled })) : (_jsx("span", { className: cn('min-h-5 min-w-0 flex-1 text-left truncate leading-5', disabled ? 'text-muted-foreground' : !selectedOption && 'text-muted-foreground'), children: displayText })), clearable && !disabled && value !== undefined && isHover ? (_jsx(Button, { variant: "text", type: "button", onClick: handleClear, className: "ml-2 flex items-center justify-center text-muted-foreground hover:text-foreground cursor-pointer", "aria-label": "Clear selection", title: "Clear", children: _jsx(X, { size: 14 }) })) : (_jsx(ChevronUp, { size: 16, className: cn('ml-2 transition-transform duration-200', isOpen ? 'rotate-0' : 'rotate-180') }))] }), _jsx(Popup, { open: isOpen, anchorRef: selectRef, contentRef: dropdownRef, positioning: positioning, keepMounted: true, onOpenChange: setIsOpen, container: popupContainer, positionMode: isParentPopupContainer ? 'absolute' : 'fixed', className: cn('w-full rounded-xl border border-border/60 bg-popover text-popover-foreground', 'shadow-[var(--shadow-floating)]', 'max-h-60 overflow-auto'), children: _jsx("div", { className: cn('w-full', !isOpen && 'hidden'), children: _jsx("div", { id: listboxId, role: "listbox", className: "p-1", children: filteredOptions.length > 0 ? (filteredOptions.map((option, index) => {
|
|
118
214
|
const isSelected = option.value === value;
|
|
119
|
-
|
|
215
|
+
const isActive = index === activeOptionIndex;
|
|
216
|
+
return (_jsx("div", { id: `${reactId}-option-${index}`, role: "option", "aria-selected": isSelected, onClick: () => handleSelect(option.value), onMouseEnter: () => setActiveOptionIndex(index), className: cn('px-4 py-2 text-sm cursor-pointer transition-colors rounded-md', 'hover:bg-foreground/8', (isSelected || isActive) && 'bg-theme/12 text-foreground'), children: option.label }, option.value));
|
|
120
217
|
})) : (_jsx("div", { className: "px-4 py-2 text-sm text-muted-foreground text-center", children: t('components.select.empty') })) }) }) })] }));
|
|
121
218
|
});
|
|
122
219
|
Select.displayName = 'Select';
|
|
@@ -3,7 +3,7 @@ import { cn } from "../../lib/utils";
|
|
|
3
3
|
import { forwardRef, useCallback } from "react";
|
|
4
4
|
import { useInputController } from "../hooks/useInputController";
|
|
5
5
|
const Textarea = forwardRef(({ className, textareaClassName, value: controlledValue, onChange, defaultValue, maxLength = 500, showCount = true, placeholder, disabled, ...props }, ref) => {
|
|
6
|
-
const { isFocused, setIsFocused, setInternalValue, setRefs, value,
|
|
6
|
+
const { isFocused, setIsFocused, setInternalValue, setRefs, value, isControlledRef, currentLength, } = useInputController({
|
|
7
7
|
controlledValue,
|
|
8
8
|
defaultValue,
|
|
9
9
|
ref,
|
|
@@ -19,9 +19,9 @@ const Textarea = forwardRef(({ className, textareaClassName, value: controlledVa
|
|
|
19
19
|
}
|
|
20
20
|
onChange?.(newValue, e);
|
|
21
21
|
}, [maxLength, isControlledRef, onChange, setInternalValue]);
|
|
22
|
-
return (_jsxs("div", { className: cn("tc-ui-textarea relative w-fit", className), children: [_jsx("textarea", { ref: setRefs, value: value, onChange: handleChange, maxLength: maxLength, placeholder: placeholder, className: cn("flex w-full rounded-lg px-
|
|
22
|
+
return (_jsxs("div", { className: cn("tc-ui-textarea relative w-fit", className), children: [_jsx("textarea", { ref: setRefs, value: value, onChange: handleChange, maxLength: maxLength, placeholder: placeholder, className: cn("flex min-h-24 w-full rounded-lg px-3 py-2 text-sm", "border transition-colors duration-200", "placeholder:text-muted-foreground", "focus:outline-none", "resize-none", showCount && "pb-10", disabled
|
|
23
23
|
? "cursor-not-allowed bg-muted border-border text-muted-foreground"
|
|
24
|
-
: "bg-background", !disabled && !
|
|
24
|
+
: "bg-background", !disabled && !isFocused && "border-border", !disabled && isFocused && "border-theme ring-2 ring-theme/25", textareaClassName), onFocus: (e) => {
|
|
25
25
|
if (!disabled) {
|
|
26
26
|
setIsFocused(true);
|
|
27
27
|
props.onFocus?.(e);
|
|
@@ -72,7 +72,7 @@ export function Tooltip({ content, children, placement = "right", visible, class
|
|
|
72
72
|
}
|
|
73
73
|
}, [isVisible, placement]);
|
|
74
74
|
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onMouseEnter: showTooltip, onMouseLeave: hideTooltip, className: "tc-ui-tooltip inline-block", children: children }), isVisible &&
|
|
75
|
-
createPortal(_jsx("div", { ref: tooltipRef, className: cn("fixed z-[
|
|
75
|
+
createPortal(_jsx("div", { ref: tooltipRef, className: cn("fixed z-[1000] px-3 py-2", "rounded-xl border border-border/50 bg-popover text-xs text-popover-foreground", "shadow-[var(--shadow-floating)]", "pointer-events-none", "max-w-xs", className), style: {
|
|
76
76
|
top: `${position.top}px`,
|
|
77
77
|
left: `${position.left}px`,
|
|
78
78
|
}, children: content }), document.body)] }));
|
|
@@ -68,8 +68,8 @@ export function ImageUpload({ accept = '.jpg,.png', maxCount = 1, value, onChang
|
|
|
68
68
|
internalUrlsRef.current = [];
|
|
69
69
|
};
|
|
70
70
|
}, []);
|
|
71
|
-
return (_jsxs("div", { className: "tc-ui-image-upload flex flex-wrap gap-3", children: [urls.map((url, index) => (_jsxs("div", { className: "relative
|
|
71
|
+
return (_jsxs("div", { className: "tc-ui-image-upload flex flex-wrap gap-3", children: [urls.map((url, index) => (_jsxs("div", { className: "relative h-24 w-24 overflow-hidden rounded-lg border border-border/60 bg-card group cursor-pointer shadow-[inset_0_1px_0_hsl(var(--foreground)/0.04)] transition-colors duration-200 hover:border-border/80 active:border-theme/60", children: [_jsx("img", { src: url, alt: "", className: "w-full h-full object-cover" }), _jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-background/0 transition-colors duration-200 group-hover:bg-background/55", children: _jsx(Button, { variant: "text", type: "button", onClick: (e) => {
|
|
72
72
|
e.stopPropagation();
|
|
73
73
|
handleRemove(index);
|
|
74
|
-
}, className: "opacity-0
|
|
74
|
+
}, className: "rounded-full bg-destructive p-1.5 text-destructive-foreground opacity-0 shadow-[var(--shadow-accent)] transition-[background-color,opacity] duration-200 hover:bg-destructive/90 active:bg-destructive/80 focus-visible:ring-destructive group-hover:opacity-100", "aria-label": t('components.upload.deleteImage'), title: t('components.upload.deleteImage'), children: _jsx(X, { size: 16 }) }) })] }, index))), urls.length < maxCount && (_jsx(FileUpload, { inputRef: inputRef, accept: mAccept, multiple: maxCount > 1, onChange: handleFileChange, labelClassName: "h-24 w-24 cursor-pointer rounded-lg border-0 p-0 focus-within:ring-2 focus-within:ring-theme/25", selectLabel: _jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-border/60 bg-card text-muted-foreground transition-colors duration-200 hover:border-border/80 hover:bg-muted/50 hover:text-foreground active:bg-muted/70", children: [_jsx(Camera, { size: 24 }), _jsx("span", { className: "text-xs", children: t('components.upload.upload') })] }) }))] }));
|
|
75
75
|
}
|
|
@@ -2,5 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Button } from '../Button';
|
|
3
3
|
import { Input } from '../Input';
|
|
4
4
|
export function FileUpload({ inputRef, accept = '*', multiple, onChange, selectLabel, labelClassName, showClear, onClear, clearLabel, }) {
|
|
5
|
-
return (_jsxs("div", { className: "tc-ui-file-upload flex items-center gap-3", children: [_jsxs("label", { className: labelClassName ??
|
|
5
|
+
return (_jsxs("div", { className: "tc-ui-file-upload flex items-center gap-3", children: [_jsxs("label", { className: labelClassName ??
|
|
6
|
+
'flex h-10 items-center gap-2 rounded-lg border border-border/60 bg-card px-4 py-2 text-sm text-card-foreground cursor-pointer transition-colors duration-200 hover:border-border/80 hover:bg-muted/50 active:bg-muted/70 focus-within:border-theme focus-within:ring-2 focus-within:ring-theme/25', children: [_jsx(Input, { ref: inputRef, type: "file", accept: accept, multiple: multiple, className: "hidden", onChange: (_, e) => onChange?.(e) }), selectLabel] }), showClear && onClear && (_jsx(Button, { size: "sm", variant: "link", onClick: onClear, children: clearLabel }))] }));
|
|
6
7
|
}
|
|
@@ -76,14 +76,14 @@
|
|
|
76
76
|
|
|
77
77
|
/* CSS 变量定义 */
|
|
78
78
|
:root {
|
|
79
|
-
--background: 0 0%
|
|
79
|
+
--background: 0 0% 99%;
|
|
80
80
|
--foreground: 240 10% 3.9%;
|
|
81
|
-
--card: 0 0%
|
|
81
|
+
--card: 0 0% 99%;
|
|
82
82
|
--card-foreground: 240 10% 3.9%;
|
|
83
|
-
--popover: 0 0%
|
|
83
|
+
--popover: 0 0% 99%;
|
|
84
84
|
--popover-foreground: 240 10% 3.9%;
|
|
85
85
|
--primary: 240 5.9% 10%;
|
|
86
|
-
--primary-foreground: 0 0%
|
|
86
|
+
--primary-foreground: 0 0% 97%;
|
|
87
87
|
--secondary: 240 4.8% 95.9%;
|
|
88
88
|
--secondary-foreground: 240 5.9% 10%;
|
|
89
89
|
--muted: 240 4.8% 95.9%;
|
|
@@ -101,10 +101,13 @@
|
|
|
101
101
|
--chart-3: 197 37% 24%;
|
|
102
102
|
--chart-4: 43 74% 66%;
|
|
103
103
|
--chart-5: 27 87% 67%;
|
|
104
|
+
--shadow-panel: 0 18px 60px hsl(240 10% 12% / 0.14);
|
|
105
|
+
--shadow-floating: 0 18px 48px hsl(240 10% 12% / 0.18), 0 1px 0 hsl(0 0% 100% / 0.82) inset;
|
|
106
|
+
--shadow-accent: 0 10px 28px hsl(240 5.9% 10% / 0.16);
|
|
104
107
|
|
|
105
108
|
/* 主题色 - 默认亮色中性主题 */
|
|
106
|
-
--theme-primary:
|
|
107
|
-
--theme-primary-foreground: 0 0%
|
|
109
|
+
--theme-primary: 240 5.9% 10%;
|
|
110
|
+
--theme-primary-foreground: 0 0% 97%;
|
|
108
111
|
--theme-bg: 210 20% 98%;
|
|
109
112
|
--theme-text: 222 47% 11%;
|
|
110
113
|
|
|
@@ -114,13 +117,13 @@
|
|
|
114
117
|
--table-head-text: 217 19% 27%;
|
|
115
118
|
--table-cell-text: 180 4% 16%;
|
|
116
119
|
--table-action-bg: 220 13% 91%;
|
|
117
|
-
--table-action-text:
|
|
120
|
+
--table-action-text: 240 5.9% 10%;
|
|
118
121
|
|
|
119
122
|
/* 面包屑颜色 - 默认亮色中性主题 */
|
|
120
123
|
--breadcrumb-text: 220 10% 46%;
|
|
121
124
|
--breadcrumb-border: 0 0% 88%;
|
|
122
|
-
--breadcrumb-active-text: 0 0%
|
|
123
|
-
--breadcrumb-active-bg:
|
|
125
|
+
--breadcrumb-active-text: 0 0% 97%;
|
|
126
|
+
--breadcrumb-active-bg: 240 5.9% 10%;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/* 绿色主题 */
|
|
@@ -169,6 +172,9 @@
|
|
|
169
172
|
--chart-3: 30 80% 55%;
|
|
170
173
|
--chart-4: 280 65% 60%;
|
|
171
174
|
--chart-5: 340 75% 55%;
|
|
175
|
+
--shadow-panel: 0 18px 60px hsl(240 10% 2% / 0.38);
|
|
176
|
+
--shadow-floating: 0 20px 64px hsl(240 10% 2% / 0.5), 0 1px 0 hsl(0 0% 100% / 0.05) inset;
|
|
177
|
+
--shadow-accent: 0 10px 28px hsl(0 0% 95% / 0.16);
|
|
172
178
|
|
|
173
179
|
/* 主题色调整 */
|
|
174
180
|
--theme-primary: 0 0% 92%;
|
|
@@ -191,6 +197,57 @@
|
|
|
191
197
|
--breadcrumb-active-bg: 0 0% 92%;
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
.dark[data-ui-style='shuttle'],
|
|
201
|
+
.dark [data-ui-style='shuttle'] {
|
|
202
|
+
--background: 240 8% 3%;
|
|
203
|
+
--foreground: 0 0% 93%;
|
|
204
|
+
|
|
205
|
+
--card: 150 5% 7%;
|
|
206
|
+
--card-foreground: 0 0% 92%;
|
|
207
|
+
|
|
208
|
+
--popover: 240 4% 14%;
|
|
209
|
+
--popover-foreground: 0 0% 94%;
|
|
210
|
+
|
|
211
|
+
--primary: 35 100% 55%;
|
|
212
|
+
--primary-foreground: 240 8% 6%;
|
|
213
|
+
--secondary: 240 4% 18%;
|
|
214
|
+
--secondary-foreground: 0 0% 94%;
|
|
215
|
+
--muted: 240 4% 18%;
|
|
216
|
+
--muted-foreground: 240 3% 58%;
|
|
217
|
+
--accent: 247 88% 70%;
|
|
218
|
+
--accent-foreground: 240 8% 6%;
|
|
219
|
+
--destructive: 0 72% 58%;
|
|
220
|
+
--destructive-foreground: 0 0% 94%;
|
|
221
|
+
--border: 240 4% 18%;
|
|
222
|
+
--input: 240 4% 16%;
|
|
223
|
+
--ring: 35 100% 58%;
|
|
224
|
+
--chart-1: 35 100% 55%;
|
|
225
|
+
--chart-2: 247 88% 70%;
|
|
226
|
+
--chart-3: 207 88% 72%;
|
|
227
|
+
--chart-4: 145 58% 52%;
|
|
228
|
+
--chart-5: 42 96% 58%;
|
|
229
|
+
--shadow-panel: 0 18px 60px hsl(240 10% 2% / 0.45);
|
|
230
|
+
--shadow-floating: 0 22px 70px hsl(240 10% 2% / 0.58), 0 1px 0 hsl(0 0% 100% / 0.06) inset;
|
|
231
|
+
--shadow-accent: 0 10px 28px hsl(35 100% 55% / 0.25);
|
|
232
|
+
|
|
233
|
+
--theme-primary: 35 100% 55%;
|
|
234
|
+
--theme-primary-foreground: 240 8% 6%;
|
|
235
|
+
--theme-bg: 240 5% 12%;
|
|
236
|
+
--theme-text: 0 0% 94%;
|
|
237
|
+
|
|
238
|
+
--table-header-bg: 240 5% 12%;
|
|
239
|
+
--table-header-text: 240 3% 58%;
|
|
240
|
+
--table-head-text: 0 0% 82%;
|
|
241
|
+
--table-cell-text: 0 0% 90%;
|
|
242
|
+
--table-action-bg: 240 4% 18%;
|
|
243
|
+
--table-action-text: 0 0% 94%;
|
|
244
|
+
|
|
245
|
+
--breadcrumb-text: 240 3% 62%;
|
|
246
|
+
--breadcrumb-border: 240 4% 18%;
|
|
247
|
+
--breadcrumb-active-text: 240 8% 6%;
|
|
248
|
+
--breadcrumb-active-bg: 35 100% 55%;
|
|
249
|
+
}
|
|
250
|
+
|
|
194
251
|
.dark[data-theme='green'],
|
|
195
252
|
.dark [data-theme='green'] {
|
|
196
253
|
--theme-primary: 142 68% 46%;
|