@meta-1/design 0.0.167 → 0.0.169
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
|
@@ -57,6 +57,7 @@ export interface DataTableProps<TData> {
|
|
|
57
57
|
showColumnVisibility?: boolean;
|
|
58
58
|
stickyColumns?: StickyColumnProps[];
|
|
59
59
|
checkbox?: boolean;
|
|
60
|
+
onRowSelectionChange?: (selectedRows: TData[]) => void;
|
|
60
61
|
rowActions?: DropdownMenuItemProps[] | ((cell: TData) => DropdownMenuItemProps[]);
|
|
61
62
|
onRowActionClick?: (item: DropdownMenuItemProps, row: Row<TData>) => void;
|
|
62
63
|
loading?: boolean;
|
|
@@ -161,6 +162,7 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
|
|
|
161
162
|
showColumnVisibility = true,
|
|
162
163
|
rowActions,
|
|
163
164
|
checkbox = false,
|
|
165
|
+
onRowSelectionChange,
|
|
164
166
|
stickyColumns = [],
|
|
165
167
|
onRowActionClick,
|
|
166
168
|
filter,
|
|
@@ -284,6 +286,14 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
|
|
|
284
286
|
},
|
|
285
287
|
});
|
|
286
288
|
|
|
289
|
+
// 当行选中状态改变时,通知外部
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (checkbox && onRowSelectionChange) {
|
|
292
|
+
const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original);
|
|
293
|
+
onRowSelectionChange(selectedRows);
|
|
294
|
+
}
|
|
295
|
+
}, [rowSelection, checkbox, onRowSelectionChange, table]);
|
|
296
|
+
|
|
287
297
|
const leftStickyColumns = useMemo<StickyColumnProps[]>(() => {
|
|
288
298
|
const columns: StickyColumnProps[] = [];
|
|
289
299
|
if (checkbox) {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# TagsInput 组件
|
|
2
|
+
|
|
3
|
+
一个功能丰富的标签输入组件,支持添加、删除标签,带有验证、最大数量限制等特性。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- ✅ 受控/非受控模式
|
|
8
|
+
- ✅ 键盘操作(Enter 添加,Backspace 删除)
|
|
9
|
+
- ✅ 自定义分隔符(默认逗号)
|
|
10
|
+
- ✅ 标签验证
|
|
11
|
+
- ✅ 最大标签数量限制
|
|
12
|
+
- ✅ 最大标签长度限制
|
|
13
|
+
- ✅ 禁止重复标签
|
|
14
|
+
- ✅ 自定义标签渲染
|
|
15
|
+
- ✅ 完整的事件回调
|
|
16
|
+
- ✅ 禁用状态
|
|
17
|
+
- ✅ 自定义样式
|
|
18
|
+
|
|
19
|
+
## 基础用法
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { TagsInput } from "@meta-1/design";
|
|
23
|
+
|
|
24
|
+
function App() {
|
|
25
|
+
const [tags, setTags] = useState<string[]>([]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<TagsInput
|
|
29
|
+
value={tags}
|
|
30
|
+
onChange={setTags}
|
|
31
|
+
placeholder="输入标签后按 Enter..."
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Props
|
|
38
|
+
|
|
39
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
40
|
+
|------|------|--------|------|
|
|
41
|
+
| value | `string[]` | - | 受控模式的标签值 |
|
|
42
|
+
| defaultValue | `string[]` | `[]` | 非受控模式的默认值 |
|
|
43
|
+
| onChange | `(value: string[]) => void` | - | 标签变化回调 |
|
|
44
|
+
| placeholder | `string` | - | 输入框占位符 |
|
|
45
|
+
| className | `string` | - | 容器类名 |
|
|
46
|
+
| inputClassName | `string` | - | 输入框类名 |
|
|
47
|
+
| tagClassName | `string` | - | 标签类名 |
|
|
48
|
+
| maxTags | `number` | - | 最大标签数量 |
|
|
49
|
+
| maxLength | `number` | - | 单个标签最大长度 |
|
|
50
|
+
| allowDuplicates | `boolean` | `false` | 是否允许重复标签 |
|
|
51
|
+
| separator | `string \| RegExp` | `","` | 分隔符 |
|
|
52
|
+
| validate | `(tag: string) => boolean` | - | 标签验证函数 |
|
|
53
|
+
| disabled | `boolean` | `false` | 是否禁用 |
|
|
54
|
+
| onTagAdd | `(tag: string) => void` | - | 添加标签回调 |
|
|
55
|
+
| onTagRemove | `(tag: string) => void` | - | 删除标签回调 |
|
|
56
|
+
| renderTag | `(tag: string, index: number, remove: () => void) => ReactNode` | - | 自定义标签渲染 |
|
|
57
|
+
|
|
58
|
+
## 使用示例
|
|
59
|
+
|
|
60
|
+
### 最大数量限制
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<TagsInput
|
|
64
|
+
maxTags={5}
|
|
65
|
+
value={tags}
|
|
66
|
+
onChange={setTags}
|
|
67
|
+
placeholder="最多 5 个标签"
|
|
68
|
+
/>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 自定义验证
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<TagsInput
|
|
75
|
+
validate={(tag) => /^[a-zA-Z0-9]+$/.test(tag)}
|
|
76
|
+
value={tags}
|
|
77
|
+
onChange={setTags}
|
|
78
|
+
placeholder="只允许字母和数字"
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 禁止重复
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<TagsInput
|
|
86
|
+
allowDuplicates={false}
|
|
87
|
+
value={tags}
|
|
88
|
+
onChange={setTags}
|
|
89
|
+
placeholder="不允许重复标签"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 自定义标签渲染
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<TagsInput
|
|
97
|
+
renderTag={(tag, index, remove) => (
|
|
98
|
+
<div className="custom-tag">
|
|
99
|
+
#{tag}
|
|
100
|
+
<button onClick={remove}>×</button>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
value={tags}
|
|
104
|
+
onChange={setTags}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 事件回调
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
<TagsInput
|
|
112
|
+
onTagAdd={(tag) => console.log("添加:", tag)}
|
|
113
|
+
onTagRemove={(tag) => console.log("删除:", tag)}
|
|
114
|
+
value={tags}
|
|
115
|
+
onChange={setTags}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 键盘操作
|
|
120
|
+
|
|
121
|
+
- **Enter**: 添加当前输入的标签
|
|
122
|
+
- **Backspace**: 当输入框为空时,删除最后一个标签
|
|
123
|
+
- **分隔符** (默认逗号): 添加标签
|
|
124
|
+
|
|
125
|
+
## 样式自定义
|
|
126
|
+
|
|
127
|
+
组件支持通过 `className`、`inputClassName` 和 `tagClassName` 来自定义样式:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<TagsInput
|
|
131
|
+
className="border-primary"
|
|
132
|
+
tagClassName="bg-primary text-primary-foreground"
|
|
133
|
+
inputClassName="text-lg"
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { forwardRef, type KeyboardEvent, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { XIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@meta-1/design/lib";
|
|
5
|
+
|
|
6
|
+
export interface TagsInputProps {
|
|
7
|
+
value?: string[];
|
|
8
|
+
defaultValue?: string[];
|
|
9
|
+
onChange?: (value: string[]) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
inputClassName?: string;
|
|
13
|
+
tagClassName?: string;
|
|
14
|
+
maxTags?: number;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
allowDuplicates?: boolean;
|
|
17
|
+
separator?: string | RegExp;
|
|
18
|
+
validate?: (tag: string) => boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onTagAdd?: (tag: string) => void;
|
|
21
|
+
onTagRemove?: (tag: string) => void;
|
|
22
|
+
renderTag?: (tag: string, index: number, remove: () => void) => React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TagsInput = forwardRef<HTMLDivElement, TagsInputProps>((props, ref) => {
|
|
26
|
+
const {
|
|
27
|
+
value,
|
|
28
|
+
defaultValue = [],
|
|
29
|
+
onChange,
|
|
30
|
+
placeholder,
|
|
31
|
+
className,
|
|
32
|
+
inputClassName,
|
|
33
|
+
tagClassName,
|
|
34
|
+
maxTags,
|
|
35
|
+
maxLength,
|
|
36
|
+
allowDuplicates = false,
|
|
37
|
+
separator = ",",
|
|
38
|
+
validate,
|
|
39
|
+
disabled = false,
|
|
40
|
+
onTagAdd,
|
|
41
|
+
onTagRemove,
|
|
42
|
+
renderTag,
|
|
43
|
+
} = props;
|
|
44
|
+
|
|
45
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
46
|
+
const [inputValue, setInputValue] = useState("");
|
|
47
|
+
const [tags, setTags] = useState<string[]>(value || defaultValue);
|
|
48
|
+
|
|
49
|
+
const isControlled = value !== undefined;
|
|
50
|
+
const currentTags = isControlled ? value : tags;
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isControlled) {
|
|
54
|
+
setTags(value);
|
|
55
|
+
}
|
|
56
|
+
}, [value, isControlled]);
|
|
57
|
+
|
|
58
|
+
const handleChange = (newTags: string[]) => {
|
|
59
|
+
if (!isControlled) {
|
|
60
|
+
setTags(newTags);
|
|
61
|
+
}
|
|
62
|
+
onChange?.(newTags);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const addTag = (tag: string) => {
|
|
66
|
+
const trimmedTag = tag.trim();
|
|
67
|
+
|
|
68
|
+
if (!trimmedTag) return;
|
|
69
|
+
|
|
70
|
+
// 验证标签
|
|
71
|
+
if (validate && !validate(trimmedTag)) return;
|
|
72
|
+
|
|
73
|
+
// 检查最大数量
|
|
74
|
+
if (maxTags && currentTags.length >= maxTags) return;
|
|
75
|
+
|
|
76
|
+
// 检查重复
|
|
77
|
+
if (!allowDuplicates && currentTags.includes(trimmedTag)) return;
|
|
78
|
+
|
|
79
|
+
const newTags = [...currentTags, trimmedTag];
|
|
80
|
+
handleChange(newTags);
|
|
81
|
+
onTagAdd?.(trimmedTag);
|
|
82
|
+
setInputValue("");
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const removeTag = (index: number) => {
|
|
86
|
+
const tag = currentTags[index];
|
|
87
|
+
const newTags = currentTags.filter((_, i) => i !== index);
|
|
88
|
+
handleChange(newTags);
|
|
89
|
+
onTagRemove?.(tag);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
93
|
+
const target = e.target as HTMLInputElement;
|
|
94
|
+
const value = target.value;
|
|
95
|
+
|
|
96
|
+
// Enter 或分隔符键
|
|
97
|
+
if (e.key === "Enter" || (typeof separator === "string" && e.key === separator)) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
if (value) {
|
|
100
|
+
addTag(value);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Backspace 删除最后一个标签
|
|
106
|
+
if (e.key === "Backspace" && !value && currentTags.length > 0) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
removeTag(currentTags.length - 1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
const value = e.target.value;
|
|
115
|
+
|
|
116
|
+
// 检查是否包含分隔符
|
|
117
|
+
if (separator) {
|
|
118
|
+
const parts = typeof separator === "string" ? value.split(separator) : value.split(separator);
|
|
119
|
+
|
|
120
|
+
if (parts.length > 1) {
|
|
121
|
+
// 添加所有非空部分(除了最后一个)
|
|
122
|
+
parts.slice(0, -1).forEach((part) => {
|
|
123
|
+
if (part.trim()) {
|
|
124
|
+
addTag(part);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// 保留最后一部分作为输入值
|
|
128
|
+
setInputValue(parts[parts.length - 1]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 检查最大长度
|
|
134
|
+
if (maxLength && value.length > maxLength) return;
|
|
135
|
+
|
|
136
|
+
setInputValue(value);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleBlur = () => {
|
|
140
|
+
if (inputValue.trim()) {
|
|
141
|
+
addTag(inputValue);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleContainerClick = () => {
|
|
146
|
+
inputRef.current?.focus();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const defaultRenderTag = (tag: string, index: number, remove: () => void) => (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
"inline-flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-sm transition-colors",
|
|
153
|
+
"hover:bg-secondary/80",
|
|
154
|
+
disabled && "pointer-events-none opacity-50",
|
|
155
|
+
tagClassName,
|
|
156
|
+
)}
|
|
157
|
+
key={`${tag}-${index}`}
|
|
158
|
+
>
|
|
159
|
+
<span className="max-w-[200px] truncate">{tag}</span>
|
|
160
|
+
{!disabled && (
|
|
161
|
+
<button
|
|
162
|
+
className="inline-flex items-center justify-center rounded-sm opacity-70 hover:opacity-100 focus:outline-none"
|
|
163
|
+
onClick={(e) => {
|
|
164
|
+
e.stopPropagation();
|
|
165
|
+
remove();
|
|
166
|
+
}}
|
|
167
|
+
type="button"
|
|
168
|
+
>
|
|
169
|
+
<XIcon className="h-3 w-3" />
|
|
170
|
+
</button>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div
|
|
177
|
+
className={cn(
|
|
178
|
+
"flex min-h-9 w-full flex-wrap gap-1.5 rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow]",
|
|
179
|
+
"focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50",
|
|
180
|
+
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
|
181
|
+
"dark:bg-input/30",
|
|
182
|
+
className,
|
|
183
|
+
)}
|
|
184
|
+
onClick={handleContainerClick}
|
|
185
|
+
ref={ref}
|
|
186
|
+
>
|
|
187
|
+
{currentTags.map((tag, index) =>
|
|
188
|
+
renderTag
|
|
189
|
+
? renderTag(tag, index, () => removeTag(index))
|
|
190
|
+
: defaultRenderTag(tag, index, () => removeTag(index)),
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
<input
|
|
194
|
+
className={cn(
|
|
195
|
+
"min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground",
|
|
196
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed",
|
|
197
|
+
inputClassName,
|
|
198
|
+
)}
|
|
199
|
+
disabled={disabled || (maxTags !== undefined && currentTags.length >= maxTags)}
|
|
200
|
+
onBlur={handleBlur}
|
|
201
|
+
onChange={handleInputChange}
|
|
202
|
+
onKeyDown={handleKeyDown}
|
|
203
|
+
placeholder={currentTags.length === 0 ? placeholder : undefined}
|
|
204
|
+
ref={inputRef}
|
|
205
|
+
type="text"
|
|
206
|
+
value={inputValue}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
TagsInput.displayName = "TagsInput";
|
package/src/index.ts
CHANGED
|
@@ -158,6 +158,8 @@ export type { StepsItemProps, StepsProps } from "./components/uix/steps";
|
|
|
158
158
|
export { Steps, StepsItem } from "./components/uix/steps";
|
|
159
159
|
export type { SwitchProps } from "./components/uix/switch";
|
|
160
160
|
export { Switch } from "./components/uix/switch";
|
|
161
|
+
export type { TagsInputProps } from "./components/uix/tags-input";
|
|
162
|
+
export { TagsInput } from "./components/uix/tags-input";
|
|
161
163
|
export type { TooltipProps } from "./components/uix/tooltip";
|
|
162
164
|
export { Tooltip } from "./components/uix/tooltip";
|
|
163
165
|
export type { TreeData, TreeProps } from "./components/uix/tree";
|