@meta-1/design 0.0.166 → 0.0.168
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.
|
|
3
|
+
"version": "0.0.168",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"easykit",
|
|
6
6
|
"design",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@radix-ui/react-alert-dialog": "1.1.2",
|
|
26
26
|
"@radix-ui/react-avatar": "1.1.1",
|
|
27
27
|
"@radix-ui/react-checkbox": "1.1.2",
|
|
28
|
+
"@radix-ui/react-context-menu": "^2.2.16",
|
|
28
29
|
"@radix-ui/react-dialog": "1.1.2",
|
|
29
30
|
"@radix-ui/react-dismissable-layer": "1.1.1",
|
|
30
31
|
"@radix-ui/react-dropdown-menu": "2.1.2",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
|
3
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@meta-1/design/lib/utils"
|
|
6
|
+
|
|
7
|
+
const ContextMenu = ContextMenuPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
|
10
|
+
|
|
11
|
+
const ContextMenuGroup = ContextMenuPrimitive.Group
|
|
12
|
+
|
|
13
|
+
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
|
14
|
+
|
|
15
|
+
const ContextMenuSub = ContextMenuPrimitive.Sub
|
|
16
|
+
|
|
17
|
+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
|
18
|
+
|
|
19
|
+
const ContextMenuSubTrigger = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
22
|
+
inset?: boolean
|
|
23
|
+
}
|
|
24
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
25
|
+
<ContextMenuPrimitive.SubTrigger
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
29
|
+
inset && "pl-8",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
36
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
37
|
+
))
|
|
38
|
+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
|
39
|
+
|
|
40
|
+
const ContextMenuSubContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<ContextMenuPrimitive.SubContent
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
))
|
|
53
|
+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
|
54
|
+
|
|
55
|
+
const ContextMenuContent = React.forwardRef<
|
|
56
|
+
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
57
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
58
|
+
>(({ className, ...props }, ref) => (
|
|
59
|
+
<ContextMenuPrimitive.Portal>
|
|
60
|
+
<ContextMenuPrimitive.Content
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn(
|
|
63
|
+
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
</ContextMenuPrimitive.Portal>
|
|
69
|
+
))
|
|
70
|
+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
|
71
|
+
|
|
72
|
+
const ContextMenuItem = React.forwardRef<
|
|
73
|
+
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
74
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
75
|
+
inset?: boolean
|
|
76
|
+
}
|
|
77
|
+
>(({ className, inset, ...props }, ref) => (
|
|
78
|
+
<ContextMenuPrimitive.Item
|
|
79
|
+
ref={ref}
|
|
80
|
+
className={cn(
|
|
81
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
82
|
+
inset && "pl-8",
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
))
|
|
88
|
+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
|
89
|
+
|
|
90
|
+
const ContextMenuCheckboxItem = React.forwardRef<
|
|
91
|
+
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
92
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
93
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
94
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
95
|
+
ref={ref}
|
|
96
|
+
className={cn(
|
|
97
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
98
|
+
className
|
|
99
|
+
)}
|
|
100
|
+
checked={checked}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
104
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
105
|
+
<Check className="h-4 w-4" />
|
|
106
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
107
|
+
</span>
|
|
108
|
+
{children}
|
|
109
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
110
|
+
))
|
|
111
|
+
ContextMenuCheckboxItem.displayName =
|
|
112
|
+
ContextMenuPrimitive.CheckboxItem.displayName
|
|
113
|
+
|
|
114
|
+
const ContextMenuRadioItem = React.forwardRef<
|
|
115
|
+
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
116
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
117
|
+
>(({ className, children, ...props }, ref) => (
|
|
118
|
+
<ContextMenuPrimitive.RadioItem
|
|
119
|
+
ref={ref}
|
|
120
|
+
className={cn(
|
|
121
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
127
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
128
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
129
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
130
|
+
</span>
|
|
131
|
+
{children}
|
|
132
|
+
</ContextMenuPrimitive.RadioItem>
|
|
133
|
+
))
|
|
134
|
+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
|
135
|
+
|
|
136
|
+
const ContextMenuLabel = React.forwardRef<
|
|
137
|
+
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
138
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
139
|
+
inset?: boolean
|
|
140
|
+
}
|
|
141
|
+
>(({ className, inset, ...props }, ref) => (
|
|
142
|
+
<ContextMenuPrimitive.Label
|
|
143
|
+
ref={ref}
|
|
144
|
+
className={cn(
|
|
145
|
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
146
|
+
inset && "pl-8",
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
))
|
|
152
|
+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
|
153
|
+
|
|
154
|
+
const ContextMenuSeparator = React.forwardRef<
|
|
155
|
+
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
156
|
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
157
|
+
>(({ className, ...props }, ref) => (
|
|
158
|
+
<ContextMenuPrimitive.Separator
|
|
159
|
+
ref={ref}
|
|
160
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
))
|
|
164
|
+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
|
165
|
+
|
|
166
|
+
const ContextMenuShortcut = ({
|
|
167
|
+
className,
|
|
168
|
+
...props
|
|
169
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
170
|
+
return (
|
|
171
|
+
<span
|
|
172
|
+
className={cn(
|
|
173
|
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
174
|
+
className
|
|
175
|
+
)}
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
ContextMenu,
|
|
184
|
+
ContextMenuTrigger,
|
|
185
|
+
ContextMenuContent,
|
|
186
|
+
ContextMenuItem,
|
|
187
|
+
ContextMenuCheckboxItem,
|
|
188
|
+
ContextMenuRadioItem,
|
|
189
|
+
ContextMenuLabel,
|
|
190
|
+
ContextMenuSeparator,
|
|
191
|
+
ContextMenuShortcut,
|
|
192
|
+
ContextMenuGroup,
|
|
193
|
+
ContextMenuPortal,
|
|
194
|
+
ContextMenuSub,
|
|
195
|
+
ContextMenuSubContent,
|
|
196
|
+
ContextMenuSubTrigger,
|
|
197
|
+
ContextMenuRadioGroup,
|
|
198
|
+
}
|
|
@@ -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
|
@@ -28,6 +28,23 @@ export {
|
|
|
28
28
|
CommandSeparator,
|
|
29
29
|
CommandShortcut,
|
|
30
30
|
} from "./components/ui/command";
|
|
31
|
+
export {
|
|
32
|
+
ContextMenu,
|
|
33
|
+
ContextMenuCheckboxItem,
|
|
34
|
+
ContextMenuContent,
|
|
35
|
+
ContextMenuGroup,
|
|
36
|
+
ContextMenuItem,
|
|
37
|
+
ContextMenuLabel,
|
|
38
|
+
ContextMenuPortal,
|
|
39
|
+
ContextMenuRadioGroup,
|
|
40
|
+
ContextMenuRadioItem,
|
|
41
|
+
ContextMenuSeparator,
|
|
42
|
+
ContextMenuShortcut,
|
|
43
|
+
ContextMenuSub,
|
|
44
|
+
ContextMenuSubContent,
|
|
45
|
+
ContextMenuSubTrigger,
|
|
46
|
+
ContextMenuTrigger,
|
|
47
|
+
} from "./components/ui/context-menu";
|
|
31
48
|
export * from "./components/ui/dropdown-menu";
|
|
32
49
|
export {
|
|
33
50
|
HoverCard,
|
|
@@ -78,10 +95,10 @@ export {
|
|
|
78
95
|
} from "./components/ui/table";
|
|
79
96
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
|
|
80
97
|
export { Textarea } from "./components/ui/textarea";
|
|
98
|
+
// extend
|
|
81
99
|
export type { ActionProps } from "./components/uix/action";
|
|
82
100
|
export { Action } from "./components/uix/action";
|
|
83
101
|
export type { AlertProps } from "./components/uix/alert";
|
|
84
|
-
// extend
|
|
85
102
|
export { Alert } from "./components/uix/alert";
|
|
86
103
|
export type { ConfirmProps } from "./components/uix/alert-dialog";
|
|
87
104
|
export { useAlert } from "./components/uix/alert-dialog";
|
|
@@ -141,6 +158,8 @@ export type { StepsItemProps, StepsProps } from "./components/uix/steps";
|
|
|
141
158
|
export { Steps, StepsItem } from "./components/uix/steps";
|
|
142
159
|
export type { SwitchProps } from "./components/uix/switch";
|
|
143
160
|
export { Switch } from "./components/uix/switch";
|
|
161
|
+
export type { TagsInputProps } from "./components/uix/tags-input";
|
|
162
|
+
export { TagsInput } from "./components/uix/tags-input";
|
|
144
163
|
export type { TooltipProps } from "./components/uix/tooltip";
|
|
145
164
|
export { Tooltip } from "./components/uix/tooltip";
|
|
146
165
|
export type { TreeData, TreeProps } from "./components/uix/tree";
|