@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meta-1/design",
3
- "version": "0.0.167",
3
+ "version": "0.0.169",
4
4
  "keywords": [
5
5
  "easykit",
6
6
  "design",
@@ -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";