taglite 0.1.0
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/LICENSE +21 -0
- package/README.md +671 -0
- package/dist/components/SimpleTagInput/SimpleTagInput.d.ts +3 -0
- package/dist/components/SimpleTagInput/SimpleTagInput.types.d.ts +28 -0
- package/dist/components/SimpleTagInput/index.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/taglite.cjs +3 -0
- package/dist/taglite.css +2 -0
- package/dist/taglite.js +409 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hossein Naderi Jafari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# taglite
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**A lightweight, dependency-free React tag input with a focused API and flexible interactions.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/taglite)
|
|
8
|
+
[](https://www.npmjs.com/package/taglite)
|
|
9
|
+
[](https://react.dev/)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
|
|
13
|
+
<br />
|
|
14
|
+
|
|
15
|
+
<img src="./assets/taglite-demo.gif" alt="taglite demo showing tags being added to the input" width="820" />
|
|
16
|
+
|
|
17
|
+
<br />
|
|
18
|
+
|
|
19
|
+
<sub>Type a tag, press Enter or comma, and keep going.</sub>
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<code>taglite</code> is a controlled React component for collecting, validating, normalizing, and removing tags. It has zero runtime dependencies, ships with TypeScript types, and supports keyboard-first workflows without imposing an autocomplete or form framework.
|
|
24
|
+
|
|
25
|
+
## Why taglite?
|
|
26
|
+
|
|
27
|
+
- **Small by default** — zero runtime dependencies and no animation or icon libraries.
|
|
28
|
+
- **Controlled and predictable** — the parent owns the tag array through <code>value</code> and <code>onChange</code>.
|
|
29
|
+
- **Flexible input rules** — custom separators, duplicate handling, normalization, validation, limits, and paste parsing.
|
|
30
|
+
- **Ready for real interfaces** — built-in themes, RTL support, read-only and disabled states, native input attributes, and forwarded refs.
|
|
31
|
+
- **Easy to extend** — custom tag, remove, and clear icons plus lifecycle callbacks for additions, removals, invalid tags, and clearing.
|
|
32
|
+
|
|
33
|
+
## Built-in themes
|
|
34
|
+
|
|
35
|
+
The component includes eight theme values: <code>default</code> (a backward-compatible alias of <code>light</code>), <code>light</code>, <code>dark</code>, <code>cupcake</code>, <code>emerald</code>, <code>corporate</code>, <code>retro</code>, and <code>dracula</code>.
|
|
36
|
+
|
|
37
|
+
<table>
|
|
38
|
+
<tr>
|
|
39
|
+
<td align="center"><strong>Light</strong><br /><img src="./assets/Demo-Light.jpg" alt="taglite light theme" width="500" /></td>
|
|
40
|
+
<td align="center"><strong>Dark</strong><br /><img src="./assets/Demo-Dark.jpg" alt="taglite dark theme" width="500" /></td>
|
|
41
|
+
</tr>
|
|
42
|
+
<tr>
|
|
43
|
+
<td align="center"><strong>Cupcake</strong><br /><img src="./assets/Demo-Cupcake.jpg" alt="taglite cupcake theme" width="500" /></td>
|
|
44
|
+
<td align="center"><strong>Emerald</strong><br /><img src="./assets/Demo-Emerald.jpg" alt="taglite emerald theme" width="500" /></td>
|
|
45
|
+
</tr>
|
|
46
|
+
<tr>
|
|
47
|
+
<td align="center"><strong>Corporate</strong><br /><img src="./assets/Demo-Corporate.jpg" alt="taglite corporate theme" width="500" /></td>
|
|
48
|
+
<td align="center"><strong>Retro</strong><br /><img src="./assets/Demo-Retro.jpg" alt="taglite retro theme" width="500" /></td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td align="center"><strong>Dracula</strong><br /><img src="./assets/Demo-Dracula.jpg" alt="taglite dracula theme" width="500" /></td>
|
|
52
|
+
<td align="center"><em>All themes use the same component API.</em></td>
|
|
53
|
+
</tr>
|
|
54
|
+
</table>
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
~~~bash
|
|
59
|
+
npm install taglite
|
|
60
|
+
~~~
|
|
61
|
+
|
|
62
|
+
Or use another package manager:
|
|
63
|
+
|
|
64
|
+
~~~bash
|
|
65
|
+
yarn add taglite
|
|
66
|
+
pnpm add taglite
|
|
67
|
+
~~~
|
|
68
|
+
|
|
69
|
+
<code>taglite</code> supports React <code>>=18</code> and includes its own public TypeScript declarations.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
~~~tsx
|
|
74
|
+
import { useState } from 'react'
|
|
75
|
+
import { SimpleTagInput } from 'taglite'
|
|
76
|
+
import 'taglite/style.css'
|
|
77
|
+
|
|
78
|
+
export default function Example() {
|
|
79
|
+
const [tags, setTags] = useState<string[]>([])
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<SimpleTagInput
|
|
83
|
+
value={tags}
|
|
84
|
+
onChange={setTags}
|
|
85
|
+
placeholder="Add a technology..."
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
~~~
|
|
90
|
+
|
|
91
|
+
The component is controlled through <code>value</code> and <code>onChange</code>; the tag array is never stored internally. The text currently being typed is temporary UI state managed by the component.
|
|
92
|
+
|
|
93
|
+
## Common interactions
|
|
94
|
+
|
|
95
|
+
### Add tags with the keyboard
|
|
96
|
+
|
|
97
|
+
By default, pressing <code>Enter</code> or <code>,</code> converts the current input into a tag. Empty values are ignored.
|
|
98
|
+
|
|
99
|
+
~~~tsx
|
|
100
|
+
<SimpleTagInput
|
|
101
|
+
value={tags}
|
|
102
|
+
onChange={setTags}
|
|
103
|
+
separators={['Enter', ',']}
|
|
104
|
+
/>
|
|
105
|
+
~~~
|
|
106
|
+
|
|
107
|
+
~~~text
|
|
108
|
+
React + Enter -> React
|
|
109
|
+
Next.js + , -> Next.js
|
|
110
|
+
~~~
|
|
111
|
+
|
|
112
|
+
### Paste multiple tags
|
|
113
|
+
|
|
114
|
+
Comma-separated, newline-separated, and mixed input can be pasted in one operation:
|
|
115
|
+
|
|
116
|
+
~~~text
|
|
117
|
+
React, Next.js
|
|
118
|
+
TypeScript, Tailwind CSS
|
|
119
|
+
~~~
|
|
120
|
+
|
|
121
|
+
~~~ts
|
|
122
|
+
['React', 'Next.js', 'TypeScript', 'Tailwind CSS']
|
|
123
|
+
~~~
|
|
124
|
+
|
|
125
|
+
The same processing pipeline applies to pasted values:
|
|
126
|
+
|
|
127
|
+
~~~text
|
|
128
|
+
normalize -> validate -> duplicate check -> maxTags -> onChange
|
|
129
|
+
~~~
|
|
130
|
+
|
|
131
|
+
### Combine features
|
|
132
|
+
|
|
133
|
+
~~~tsx
|
|
134
|
+
import { useState } from 'react'
|
|
135
|
+
import { SimpleTagInput } from 'taglite'
|
|
136
|
+
|
|
137
|
+
export default function Example() {
|
|
138
|
+
const [tags, setTags] = useState<string[]>([])
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<SimpleTagInput
|
|
142
|
+
value={tags}
|
|
143
|
+
onChange={setTags}
|
|
144
|
+
placeholder="Add a technology..."
|
|
145
|
+
hintText="Enter, comma, or paste multiple tags"
|
|
146
|
+
separators={['Enter', ',']}
|
|
147
|
+
maxTags={8}
|
|
148
|
+
allowDuplicates={false}
|
|
149
|
+
normalizeTag={tag => tag.trim().toLowerCase()}
|
|
150
|
+
validateTag={tag =>
|
|
151
|
+
tag.length >= 2
|
|
152
|
+
? true
|
|
153
|
+
: 'Tag must contain at least 2 characters'
|
|
154
|
+
}
|
|
155
|
+
onInvalidTag={(tag, reason) => {
|
|
156
|
+
console.log('Invalid tag:', tag, reason)
|
|
157
|
+
}}
|
|
158
|
+
onTagAdd={(tag, index) => {
|
|
159
|
+
console.log('Added:', tag, index)
|
|
160
|
+
}}
|
|
161
|
+
onTagRemove={(tag, index) => {
|
|
162
|
+
console.log('Removed:', tag, index)
|
|
163
|
+
}}
|
|
164
|
+
acceptOnBlur
|
|
165
|
+
clearable
|
|
166
|
+
onClear={() => console.log('All tags cleared')}
|
|
167
|
+
theme="dark"
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
~~~
|
|
172
|
+
|
|
173
|
+
## API reference
|
|
174
|
+
|
|
175
|
+
### Core props
|
|
176
|
+
|
|
177
|
+
| Prop | Type | Default | Description |
|
|
178
|
+
| --- | --- | --- | --- |
|
|
179
|
+
| <code>value</code> | <code>string[]</code> | required | Controlled tag list |
|
|
180
|
+
| <code>onChange</code> | <code>(tags: string[]) => void</code> | required | Called when tags change |
|
|
181
|
+
| <code>direction</code> | <code>'ltr' or 'rtl'</code> | <code>'ltr'</code> | Text direction |
|
|
182
|
+
| <code>theme</code> | <code>SimpleTagInputTheme</code> | <code>'light'</code> | Built-in visual theme |
|
|
183
|
+
| <code>placeholder</code> | <code>string</code> | <code>'Add a new tag...'</code> | Input placeholder |
|
|
184
|
+
| <code>hintText</code> | <code>ReactNode</code> | <code>'Press Enter to add a tag'</code> | Focus helper text |
|
|
185
|
+
| <code>separators</code> | <code>string[]</code> | <code>['Enter', ',']</code> | Keys that create tags |
|
|
186
|
+
| <code>maxTags</code> | <code>number</code> | — | Maximum number of tags |
|
|
187
|
+
| <code>allowDuplicates</code> | <code>boolean</code> | <code>false</code> | Allow duplicate tags |
|
|
188
|
+
| <code>normalizeTag</code> | <code>(tag: string) => string</code> | — | Normalizes tags before validation |
|
|
189
|
+
| <code>validateTag</code> | <code>(tag: string) => boolean or string</code> | — | Validates tags |
|
|
190
|
+
| <code>onInvalidTag</code> | <code>(tag, reason?) => void</code> | — | Called for validation failures |
|
|
191
|
+
| <code>onTagAdd</code> | <code>(tag, index) => void</code> | — | Called after a tag is added |
|
|
192
|
+
| <code>onTagRemove</code> | <code>(tag, index) => void</code> | — | Called after a tag is removed |
|
|
193
|
+
| <code>acceptOnBlur</code> | <code>boolean</code> | <code>false</code> | Add current input on blur |
|
|
194
|
+
| <code>clearable</code> | <code>boolean</code> | <code>false</code> | Show clear-all button |
|
|
195
|
+
| <code>clearIcon</code> | <code>ReactNode</code> | built-in SVG | Custom clear icon |
|
|
196
|
+
| <code>onClear</code> | <code>() => void</code> | — | Called after clearing all tags |
|
|
197
|
+
| <code>tagIcon</code> | <code>ReactNode</code> | built-in SVG | Custom tag icon |
|
|
198
|
+
| <code>removeIcon</code> | <code>ReactNode</code> | built-in SVG | Custom remove icon |
|
|
199
|
+
| <code>removeButtonProps</code> | button attributes | — | Additional remove-button attributes |
|
|
200
|
+
| <code>readOnly</code> | native input prop | <code>false</code> | Prevent tag editing |
|
|
201
|
+
| <code>disabled</code> | native input prop | <code>false</code> | Disable interaction |
|
|
202
|
+
| <code>ref</code> | <code>Ref<HTMLInputElement></code> | — | Ref to the native input |
|
|
203
|
+
|
|
204
|
+
<code>SimpleTagInput</code> also accepts standard <code>InputHTMLAttributes<HTMLInputElement></code> props unless they conflict with the controlled <code>value</code> and tag-level <code>onChange</code> API.
|
|
205
|
+
|
|
206
|
+
### <code>value</code> and <code>onChange</code>
|
|
207
|
+
|
|
208
|
+
~~~ts
|
|
209
|
+
value: string[]
|
|
210
|
+
onChange: (tags: string[]) => void
|
|
211
|
+
~~~
|
|
212
|
+
|
|
213
|
+
<code>value</code> is the source of truth. <code>onChange</code> is called whenever the list changes, and the component does not mutate the existing array.
|
|
214
|
+
|
|
215
|
+
~~~tsx
|
|
216
|
+
const [tags, setTags] = useState<string[]>([
|
|
217
|
+
'React',
|
|
218
|
+
'Next.js',
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
<SimpleTagInput
|
|
222
|
+
value={tags}
|
|
223
|
+
onChange={setTags}
|
|
224
|
+
/>
|
|
225
|
+
~~~
|
|
226
|
+
|
|
227
|
+
### <code>direction</code>
|
|
228
|
+
|
|
229
|
+
~~~ts
|
|
230
|
+
direction?: 'ltr' | 'rtl'
|
|
231
|
+
~~~
|
|
232
|
+
|
|
233
|
+
Controls text direction. Use <code>rtl</code> for Persian, Arabic, Hebrew, and other right-to-left interfaces.
|
|
234
|
+
|
|
235
|
+
~~~tsx
|
|
236
|
+
<SimpleTagInput
|
|
237
|
+
direction="rtl"
|
|
238
|
+
value={tags}
|
|
239
|
+
onChange={setTags}
|
|
240
|
+
/>
|
|
241
|
+
~~~
|
|
242
|
+
|
|
243
|
+
### <code>theme</code>
|
|
244
|
+
|
|
245
|
+
~~~ts
|
|
246
|
+
theme?:
|
|
247
|
+
| 'default'
|
|
248
|
+
| 'light'
|
|
249
|
+
| 'dark'
|
|
250
|
+
| 'cupcake'
|
|
251
|
+
| 'emerald'
|
|
252
|
+
| 'corporate'
|
|
253
|
+
| 'retro'
|
|
254
|
+
| 'dracula'
|
|
255
|
+
~~~
|
|
256
|
+
|
|
257
|
+
<code>default</code> is a backward-compatible alias of <code>light</code>. The default theme is <code>light</code>.
|
|
258
|
+
|
|
259
|
+
~~~tsx
|
|
260
|
+
<SimpleTagInput
|
|
261
|
+
theme="dracula"
|
|
262
|
+
value={tags}
|
|
263
|
+
onChange={setTags}
|
|
264
|
+
/>
|
|
265
|
+
~~~
|
|
266
|
+
|
|
267
|
+
### <code>placeholder</code> and <code>hintText</code>
|
|
268
|
+
|
|
269
|
+
~~~ts
|
|
270
|
+
placeholder?: string
|
|
271
|
+
hintText?: ReactNode
|
|
272
|
+
~~~
|
|
273
|
+
|
|
274
|
+
The placeholder appears inside the input. <code>hintText</code> is rendered in the helper area while the component is focused.
|
|
275
|
+
|
|
276
|
+
~~~tsx
|
|
277
|
+
<SimpleTagInput
|
|
278
|
+
placeholder="Add technology..."
|
|
279
|
+
hintText="Press Enter or comma to add a tag"
|
|
280
|
+
value={tags}
|
|
281
|
+
onChange={setTags}
|
|
282
|
+
/>
|
|
283
|
+
~~~
|
|
284
|
+
|
|
285
|
+
<code>hintText</code> also accepts JSX:
|
|
286
|
+
|
|
287
|
+
~~~tsx
|
|
288
|
+
<SimpleTagInput
|
|
289
|
+
hintText={<span>Add your technology tags</span>}
|
|
290
|
+
value={tags}
|
|
291
|
+
onChange={setTags}
|
|
292
|
+
/>
|
|
293
|
+
~~~
|
|
294
|
+
|
|
295
|
+
### <code>separators</code>
|
|
296
|
+
|
|
297
|
+
~~~ts
|
|
298
|
+
separators?: string[]
|
|
299
|
+
~~~
|
|
300
|
+
|
|
301
|
+
Defines which <code>KeyboardEvent.key</code> values create a tag. The default is <code>['Enter', ',']</code>.
|
|
302
|
+
|
|
303
|
+
~~~tsx
|
|
304
|
+
<SimpleTagInput separators={['Enter']} value={tags} onChange={setTags} />
|
|
305
|
+
<SimpleTagInput separators={['Enter', 'Tab']} value={tags} onChange={setTags} />
|
|
306
|
+
<SimpleTagInput separators={['Enter', ';']} value={tags} onChange={setTags} />
|
|
307
|
+
~~~
|
|
308
|
+
|
|
309
|
+
### <code>maxTags</code> and <code>allowDuplicates</code>
|
|
310
|
+
|
|
311
|
+
~~~ts
|
|
312
|
+
maxTags?: number
|
|
313
|
+
allowDuplicates?: boolean
|
|
314
|
+
~~~
|
|
315
|
+
|
|
316
|
+
<code>maxTags</code> ignores additional tags after the limit is reached; existing tags are never removed automatically. <code>allowDuplicates</code> defaults to <code>false</code> and controls whether an already-present tag can be added again.
|
|
317
|
+
|
|
318
|
+
~~~tsx
|
|
319
|
+
<SimpleTagInput
|
|
320
|
+
value={tags}
|
|
321
|
+
onChange={setTags}
|
|
322
|
+
maxTags={5}
|
|
323
|
+
allowDuplicates={false}
|
|
324
|
+
/>
|
|
325
|
+
~~~
|
|
326
|
+
|
|
327
|
+
With duplicates disabled, adding <code>React</code>, <code>React</code>, and <code>React</code> produces one tag. With <code>allowDuplicates</code> enabled, all three can be added.
|
|
328
|
+
|
|
329
|
+
### <code>normalizeTag</code>
|
|
330
|
+
|
|
331
|
+
~~~ts
|
|
332
|
+
normalizeTag?: (tag: string) => string
|
|
333
|
+
~~~
|
|
334
|
+
|
|
335
|
+
Transforms a tag before validation and duplicate checking. This is useful for trimming or normalizing case:
|
|
336
|
+
|
|
337
|
+
~~~tsx
|
|
338
|
+
<SimpleTagInput
|
|
339
|
+
value={tags}
|
|
340
|
+
onChange={setTags}
|
|
341
|
+
normalizeTag={tag => tag.trim().toLowerCase()}
|
|
342
|
+
/>
|
|
343
|
+
~~~
|
|
344
|
+
|
|
345
|
+
Input <code> REACT</code> becomes <code>react</code>. With <code>allowDuplicates={false}</code>, <code>React</code>, <code>react</code>, and <code>REACT</code> are treated as the same tag when using <code>tag => tag.toLowerCase()</code>.
|
|
346
|
+
|
|
347
|
+
### <code>validateTag</code> and <code>onInvalidTag</code>
|
|
348
|
+
|
|
349
|
+
~~~ts
|
|
350
|
+
validateTag?: (tag: string) => boolean | string
|
|
351
|
+
onInvalidTag?: (tag: string, reason?: string) => void
|
|
352
|
+
~~~
|
|
353
|
+
|
|
354
|
+
Validation runs after normalization. Return <code>true</code> to accept a tag, <code>false</code> to reject it without a reason, or a string to reject it and provide that reason.
|
|
355
|
+
|
|
356
|
+
~~~tsx
|
|
357
|
+
<SimpleTagInput
|
|
358
|
+
value={tags}
|
|
359
|
+
onChange={setTags}
|
|
360
|
+
validateTag={tag =>
|
|
361
|
+
tag.length >= 3
|
|
362
|
+
? true
|
|
363
|
+
: 'Tag must contain at least 3 characters'
|
|
364
|
+
}
|
|
365
|
+
onInvalidTag={(tag, reason) => {
|
|
366
|
+
console.log(tag, reason)
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
~~~
|
|
370
|
+
|
|
371
|
+
### <code>onTagAdd</code> and <code>onTagRemove</code>
|
|
372
|
+
|
|
373
|
+
~~~ts
|
|
374
|
+
onTagAdd?: (tag: string, index: number) => void
|
|
375
|
+
onTagRemove?: (tag: string, index: number) => void
|
|
376
|
+
~~~
|
|
377
|
+
|
|
378
|
+
<code>onTagAdd</code> receives the final normalized tag and its resulting index. It is not called for empty, invalid, duplicate, or max-limit-rejected tags.
|
|
379
|
+
|
|
380
|
+
<code>onTagRemove</code> is triggered by clicking a tag's remove button or pressing Backspace while the input is empty. Its index is the tag's index before removal.
|
|
381
|
+
|
|
382
|
+
~~~tsx
|
|
383
|
+
<SimpleTagInput
|
|
384
|
+
value={tags}
|
|
385
|
+
onChange={setTags}
|
|
386
|
+
onTagAdd={(tag, index) => console.log('Added:', tag, index)}
|
|
387
|
+
onTagRemove={(tag, index) => console.log('Removed:', tag, index)}
|
|
388
|
+
/>
|
|
389
|
+
~~~
|
|
390
|
+
|
|
391
|
+
### <code>acceptOnBlur</code>
|
|
392
|
+
|
|
393
|
+
~~~ts
|
|
394
|
+
acceptOnBlur?: boolean
|
|
395
|
+
~~~
|
|
396
|
+
|
|
397
|
+
When enabled, the component attempts to add the current input when focus leaves it. The same normalization, validation, duplicate, and <code>maxTags</code> rules apply.
|
|
398
|
+
|
|
399
|
+
~~~tsx
|
|
400
|
+
<SimpleTagInput
|
|
401
|
+
value={tags}
|
|
402
|
+
onChange={setTags}
|
|
403
|
+
acceptOnBlur
|
|
404
|
+
/>
|
|
405
|
+
~~~
|
|
406
|
+
|
|
407
|
+
### <code>clearable</code>, <code>onClear</code>, and <code>clearIcon</code>
|
|
408
|
+
|
|
409
|
+
~~~ts
|
|
410
|
+
clearable?: boolean
|
|
411
|
+
onClear?: () => void
|
|
412
|
+
clearIcon?: ReactNode
|
|
413
|
+
~~~
|
|
414
|
+
|
|
415
|
+
<code>clearable</code> shows a clear-all button when at least one tag exists. Clicking it calls <code>onChange([])</code> and then <code>onClear</code>, if provided. The clear action is disabled in read-only and disabled modes.
|
|
416
|
+
|
|
417
|
+
~~~tsx
|
|
418
|
+
<SimpleTagInput
|
|
419
|
+
value={tags}
|
|
420
|
+
onChange={setTags}
|
|
421
|
+
clearable
|
|
422
|
+
onClear={() => console.log('All tags cleared')}
|
|
423
|
+
clearIcon={<span aria-hidden="true">×</span>}
|
|
424
|
+
/>
|
|
425
|
+
~~~
|
|
426
|
+
|
|
427
|
+
### <code>tagIcon</code> and <code>removeIcon</code>
|
|
428
|
+
|
|
429
|
+
~~~ts
|
|
430
|
+
tagIcon?: ReactNode
|
|
431
|
+
removeIcon?: ReactNode
|
|
432
|
+
~~~
|
|
433
|
+
|
|
434
|
+
Both props accept any React node. A runtime icon dependency is not required by <code>taglite</code>.
|
|
435
|
+
|
|
436
|
+
~~~tsx
|
|
437
|
+
<SimpleTagInput
|
|
438
|
+
value={tags}
|
|
439
|
+
onChange={setTags}
|
|
440
|
+
tagIcon={<span aria-hidden="true">#</span>}
|
|
441
|
+
removeIcon={<span aria-hidden="true">×</span>}
|
|
442
|
+
/>
|
|
443
|
+
~~~
|
|
444
|
+
|
|
445
|
+
You can also provide your own SVG:
|
|
446
|
+
|
|
447
|
+
~~~tsx
|
|
448
|
+
<SimpleTagInput
|
|
449
|
+
value={tags}
|
|
450
|
+
onChange={setTags}
|
|
451
|
+
tagIcon={
|
|
452
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
453
|
+
{/* ... */}
|
|
454
|
+
</svg>
|
|
455
|
+
}
|
|
456
|
+
/>
|
|
457
|
+
~~~
|
|
458
|
+
|
|
459
|
+
### <code>removeButtonProps</code>
|
|
460
|
+
|
|
461
|
+
~~~ts
|
|
462
|
+
removeButtonProps?: {
|
|
463
|
+
className?: string
|
|
464
|
+
[key: string]: unknown
|
|
465
|
+
}
|
|
466
|
+
~~~
|
|
467
|
+
|
|
468
|
+
Adds attributes to the remove buttons rendered inside tags. <code>taglite</code> keeps control of the button type, click handler, disabled state, and core behavior.
|
|
469
|
+
|
|
470
|
+
~~~tsx
|
|
471
|
+
<SimpleTagInput
|
|
472
|
+
value={tags}
|
|
473
|
+
onChange={setTags}
|
|
474
|
+
removeButtonProps={{
|
|
475
|
+
title: 'Remove tag',
|
|
476
|
+
className: 'text-red-500',
|
|
477
|
+
}}
|
|
478
|
+
/>
|
|
479
|
+
~~~
|
|
480
|
+
|
|
481
|
+
## Read-only, disabled, and native input props
|
|
482
|
+
|
|
483
|
+
### <code>readOnly</code>
|
|
484
|
+
|
|
485
|
+
<code>readOnly</code> is inherited from native input attributes. In read-only mode, new tags cannot be added, existing tags cannot be removed, Backspace and blur do not modify tags, and clear-all is disabled. The input can still be focused.
|
|
486
|
+
|
|
487
|
+
~~~tsx
|
|
488
|
+
<SimpleTagInput value={tags} onChange={setTags} readOnly />
|
|
489
|
+
~~~
|
|
490
|
+
|
|
491
|
+
### <code>disabled</code>
|
|
492
|
+
|
|
493
|
+
In disabled mode, the input, tag actions, clear-all action, and keyboard tag actions are disabled. The component does not force focus onto the disabled input.
|
|
494
|
+
|
|
495
|
+
~~~tsx
|
|
496
|
+
<SimpleTagInput value={tags} onChange={setTags} disabled />
|
|
497
|
+
~~~
|
|
498
|
+
|
|
499
|
+
### Native input attributes
|
|
500
|
+
|
|
501
|
+
<code>SimpleTagInput</code> extends <code>InputHTMLAttributes<HTMLInputElement></code>, so standard attributes are supported unless they conflict with the controlled tag API.
|
|
502
|
+
|
|
503
|
+
~~~tsx
|
|
504
|
+
<SimpleTagInput
|
|
505
|
+
value={tags}
|
|
506
|
+
onChange={setTags}
|
|
507
|
+
name="tags"
|
|
508
|
+
id="project-tags"
|
|
509
|
+
autoComplete="off"
|
|
510
|
+
autoFocus
|
|
511
|
+
required
|
|
512
|
+
/>
|
|
513
|
+
~~~
|
|
514
|
+
|
|
515
|
+
The component intentionally controls <code>value</code> and <code>onChange</code> as its tag-list API.
|
|
516
|
+
|
|
517
|
+
## Forwarded ref
|
|
518
|
+
|
|
519
|
+
The component forwards its ref directly to the underlying native <code><input></code> element. This is useful for forms, dialogs, keyboard shortcuts, and programmatic focus management.
|
|
520
|
+
|
|
521
|
+
~~~tsx
|
|
522
|
+
import { useRef } from 'react'
|
|
523
|
+
import { SimpleTagInput } from 'taglite'
|
|
524
|
+
|
|
525
|
+
export default function Example() {
|
|
526
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<>
|
|
530
|
+
<SimpleTagInput
|
|
531
|
+
ref={inputRef}
|
|
532
|
+
value={tags}
|
|
533
|
+
onChange={setTags}
|
|
534
|
+
/>
|
|
535
|
+
|
|
536
|
+
<button
|
|
537
|
+
type="button"
|
|
538
|
+
onClick={() => inputRef.current?.focus()}
|
|
539
|
+
>
|
|
540
|
+
Focus input
|
|
541
|
+
</button>
|
|
542
|
+
</>
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
~~~
|
|
546
|
+
|
|
547
|
+
## Common patterns
|
|
548
|
+
|
|
549
|
+
### Technology tags
|
|
550
|
+
|
|
551
|
+
~~~tsx
|
|
552
|
+
<SimpleTagInput
|
|
553
|
+
value={technologies}
|
|
554
|
+
onChange={setTechnologies}
|
|
555
|
+
placeholder="Add technology..."
|
|
556
|
+
normalizeTag={tag => tag.trim()}
|
|
557
|
+
maxTags={10}
|
|
558
|
+
/>
|
|
559
|
+
~~~
|
|
560
|
+
|
|
561
|
+
### Product keywords
|
|
562
|
+
|
|
563
|
+
~~~tsx
|
|
564
|
+
<SimpleTagInput
|
|
565
|
+
value={keywords}
|
|
566
|
+
onChange={setKeywords}
|
|
567
|
+
placeholder="Add keyword..."
|
|
568
|
+
allowDuplicates={false}
|
|
569
|
+
/>
|
|
570
|
+
~~~
|
|
571
|
+
|
|
572
|
+
### RTL / Persian
|
|
573
|
+
|
|
574
|
+
~~~tsx
|
|
575
|
+
<SimpleTagInput
|
|
576
|
+
direction="rtl"
|
|
577
|
+
theme="light"
|
|
578
|
+
value={tags}
|
|
579
|
+
onChange={setTags}
|
|
580
|
+
placeholder="برچسب جدید..."
|
|
581
|
+
hintText="برای افزودن برچسب Enter را بزنید"
|
|
582
|
+
/>
|
|
583
|
+
~~~
|
|
584
|
+
|
|
585
|
+
### Strict validation
|
|
586
|
+
|
|
587
|
+
~~~tsx
|
|
588
|
+
<SimpleTagInput
|
|
589
|
+
value={tags}
|
|
590
|
+
onChange={setTags}
|
|
591
|
+
validateTag={tag =>
|
|
592
|
+
/^[a-z0-9-]+$/i.test(tag)
|
|
593
|
+
? true
|
|
594
|
+
: 'Only letters, numbers, and hyphens are allowed'
|
|
595
|
+
}
|
|
596
|
+
/>
|
|
597
|
+
~~~
|
|
598
|
+
|
|
599
|
+
## Accessibility
|
|
600
|
+
|
|
601
|
+
The component uses native HTML controls and provides accessible labels for tag removal buttons. The default remove button receives a label based on the tag name, such as:
|
|
602
|
+
|
|
603
|
+
~~~text
|
|
604
|
+
Remove tag React
|
|
605
|
+
~~~
|
|
606
|
+
|
|
607
|
+
When replacing icons with custom React nodes, keep decorative icons <code>aria-hidden</code> when the icon itself does not provide information. For form-level labels, descriptions, and validation messages, use your application's surrounding form or field structure rather than duplicating field abstractions inside <code>SimpleTagInput</code>.
|
|
608
|
+
|
|
609
|
+
## TypeScript
|
|
610
|
+
|
|
611
|
+
The package exposes its public component and theme types:
|
|
612
|
+
|
|
613
|
+
~~~tsx
|
|
614
|
+
import {
|
|
615
|
+
SimpleTagInput,
|
|
616
|
+
type SimpleTagInputProps,
|
|
617
|
+
type SimpleTagInputTheme,
|
|
618
|
+
} from 'taglite'
|
|
619
|
+
~~~
|
|
620
|
+
|
|
621
|
+
~~~ts
|
|
622
|
+
const theme: SimpleTagInputTheme = 'dracula'
|
|
623
|
+
|
|
624
|
+
const props: SimpleTagInputProps = {
|
|
625
|
+
value: [],
|
|
626
|
+
onChange: tags => {
|
|
627
|
+
console.log(tags)
|
|
628
|
+
},
|
|
629
|
+
theme,
|
|
630
|
+
}
|
|
631
|
+
~~~
|
|
632
|
+
|
|
633
|
+
## Performance notes
|
|
634
|
+
|
|
635
|
+
<code>taglite</code> is designed to stay small and lightweight:
|
|
636
|
+
|
|
637
|
+
- no runtime dependencies
|
|
638
|
+
- inline SVG icons instead of an icon package
|
|
639
|
+
- controlled tag state managed by the parent
|
|
640
|
+
- memoized tag rendering
|
|
641
|
+
- cached theme styles
|
|
642
|
+
- simple array operations for normal add/remove flows
|
|
643
|
+
- no animation library
|
|
644
|
+
- no built-in network or asynchronous logic
|
|
645
|
+
|
|
646
|
+
For large tag collections, keep the <code>value</code> reference stable when the tags themselves have not changed.
|
|
647
|
+
|
|
648
|
+
## Development
|
|
649
|
+
|
|
650
|
+
Clone the repository, install dependencies, and start the Vite development server:
|
|
651
|
+
|
|
652
|
+
~~~bash
|
|
653
|
+
npm install
|
|
654
|
+
npm run dev
|
|
655
|
+
~~~
|
|
656
|
+
|
|
657
|
+
Available scripts:
|
|
658
|
+
|
|
659
|
+
| Command | Purpose |
|
|
660
|
+
| --- | --- |
|
|
661
|
+
| <code>npm run dev</code> | Start the Vite dev server with HMR |
|
|
662
|
+
| <code>npm run build</code> | Type-check and create the production app bundle |
|
|
663
|
+
| <code>npm run build:lib</code> | Build the distributable library and declaration files |
|
|
664
|
+
| <code>npm run lint</code> | Run ESLint across the repository |
|
|
665
|
+
| <code>npm run preview</code> | Preview the production build locally |
|
|
666
|
+
|
|
667
|
+
There is currently no automated test runner or <code>npm test</code> script. When behavior grows beyond manual verification, add focused component tests covering tag creation with Enter/comma, duplicate handling, removal, keyboard behavior, focus states, and each supported theme.
|
|
668
|
+
|
|
669
|
+
## License
|
|
670
|
+
|
|
671
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export type SimpleTagInputTheme = 'default' | 'light' | 'dark' | 'cupcake' | 'emerald' | 'corporate' | 'retro' | 'dracula';
|
|
3
|
+
export interface SimpleTagInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
|
|
4
|
+
className?: string;
|
|
5
|
+
value: string[];
|
|
6
|
+
onChange: (tags: string[]) => void;
|
|
7
|
+
direction?: 'ltr' | 'rtl';
|
|
8
|
+
theme?: SimpleTagInputTheme;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
hintText?: ReactNode;
|
|
11
|
+
tagIcon?: ReactNode;
|
|
12
|
+
removeIcon?: ReactNode;
|
|
13
|
+
clearIcon?: ReactNode;
|
|
14
|
+
separators?: string[];
|
|
15
|
+
maxTags?: number;
|
|
16
|
+
allowDuplicates?: boolean;
|
|
17
|
+
normalizeTag?: (tag: string) => string;
|
|
18
|
+
validateTag?: (tag: string) => boolean | string;
|
|
19
|
+
onInvalidTag?: (tag: string, reason?: string) => void;
|
|
20
|
+
onTagAdd?: (tag: string, index: number) => void;
|
|
21
|
+
onTagRemove?: (tag: string, index: number) => void;
|
|
22
|
+
acceptOnBlur?: boolean;
|
|
23
|
+
clearable?: boolean;
|
|
24
|
+
onClear?: () => void;
|
|
25
|
+
removeButtonProps?: Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'onClick' | 'disabled' | 'className'> & {
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/taglite.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react/jsx-runtime");var n={background:`#ffffff`,backgroundHover:`#ffffff`,border:`#d1d5db`,borderFocus:`#111827`,ring:`rgba(17, 24, 39, 0.10)`,text:`#111827`,placeholder:`#9ca3af`,mutedText:`#9ca3af`,tagBackground:`#f3f4f6`,tagBorder:`#e5e7eb`,tagText:`#374151`,tagHoverBackground:`#e5e7eb`,tagHoverBorder:`#d1d5db`,removeText:`#6b7280`,removeHoverBackground:`#e5e7eb`,removeHoverText:`#111827`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 1px 2px rgba(0, 0, 0, 0.03)`,focusShadow:`0 0 0 3px rgba(17, 24, 39, 0.10)`},r={default:n,light:n,dark:{background:`#111827`,backgroundHover:`#111827`,border:`#374151`,borderFocus:`#f9fafb`,ring:`rgba(249, 250, 251, 0.10)`,text:`#f9fafb`,placeholder:`#6b7280`,mutedText:`#6b7280`,tagBackground:`#1f2937`,tagBorder:`#374151`,tagText:`#e5e7eb`,tagHoverBackground:`#374151`,tagHoverBorder:`#4b5563`,removeText:`#9ca3af`,removeHoverBackground:`#374151`,removeHoverText:`#f9fafb`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 1px 2px rgba(0, 0, 0, 0.30)`,focusShadow:`0 0 0 3px rgba(249, 250, 251, 0.10)`},cupcake:{background:`#fff7f3`,backgroundHover:`#fffaf7`,border:`#f0c8c0`,borderFocus:`#e6a89c`,ring:`rgba(230, 168, 156, 0.20)`,text:`#5c4b51`,placeholder:`#a9959c`,mutedText:`#a9959c`,tagBackground:`#f9dfe5`,tagBorder:`#efc4ce`,tagText:`#7b4f5b`,tagHoverBackground:`#f6d2da`,tagHoverBorder:`#e8b4c0`,removeText:`#a86b78`,removeHoverBackground:`#efc4ce`,removeHoverText:`#693f4b`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 2px 8px rgba(92, 75, 81, 0.06)`,focusShadow:`0 0 0 3px rgba(230, 168, 156, 0.20)`},emerald:{background:`#ffffff`,backgroundHover:`#fafffd`,border:`#a7d8c5`,borderFocus:`#10b981`,ring:`rgba(16, 185, 129, 0.14)`,text:`#163c31`,placeholder:`#7ca396`,mutedText:`#7ca396`,tagBackground:`#dff7ec`,tagBorder:`#b8ead4`,tagText:`#087f5b`,tagHoverBackground:`#cef1e1`,tagHoverBorder:`#9fdfc5`,removeText:`#2f9678`,removeHoverBackground:`#b8ead4`,removeHoverText:`#087f5b`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 1px 3px rgba(16, 185, 129, 0.08)`,focusShadow:`0 0 0 3px rgba(16, 185, 129, 0.14)`},corporate:{background:`#ffffff`,backgroundHover:`#ffffff`,border:`#cbd5e1`,borderFocus:`#1d4ed8`,ring:`rgba(29, 78, 216, 0.12)`,text:`#0f172a`,placeholder:`#94a3b8`,mutedText:`#94a3b8`,tagBackground:`#eff6ff`,tagBorder:`#bfdbfe`,tagText:`#1d4ed8`,tagHoverBackground:`#dbeafe`,tagHoverBorder:`#93c5fd`,removeText:`#64748b`,removeHoverBackground:`#dbeafe`,removeHoverText:`#1d4ed8`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 1px 2px rgba(15, 23, 42, 0.04)`,focusShadow:`0 0 0 3px rgba(29, 78, 216, 0.12)`},retro:{background:`#fdf6e3`,backgroundHover:`#fffaf0`,border:`#b8a27a`,borderFocus:`#9f1239`,ring:`rgba(159, 18, 57, 0.14)`,text:`#3f2f23`,placeholder:`#9b8b73`,mutedText:`#9b8b73`,tagBackground:`#f4d8a8`,tagBorder:`#d9b77a`,tagText:`#5c3b16`,tagHoverBackground:`#efd09a`,tagHoverBorder:`#cda967`,removeText:`#8b5e34`,removeHoverBackground:`#e7bf7e`,removeHoverText:`#5c3b16`,radius:`1rem`,tagRadius:`1rem`,shadow:`2px 2px 0 rgba(63, 47, 35, 0.12)`,focusShadow:`0 0 0 3px rgba(159, 18, 57, 0.14)`},dracula:{background:`#282a36`,backgroundHover:`#2d2f3b`,border:`#44475a`,borderFocus:`#bd93f9`,ring:`rgba(189, 147, 249, 0.18)`,text:`#f8f8f2`,placeholder:`#6272a4`,mutedText:`#6272a4`,tagBackground:`#44475a`,tagBorder:`#6272a4`,tagText:`#f8f8f2`,tagHoverBackground:`#4f5266`,tagHoverBorder:`#bd93f9`,removeText:`#bd93f9`,removeHoverBackground:`#6272a4`,removeHoverText:`#f8f8f2`,radius:`1rem`,tagRadius:`1rem`,shadow:`0 2px 8px rgba(0, 0, 0, 0.30)`,focusShadow:`0 0 0 3px rgba(189, 147, 249, 0.18)`}};function i(e){return{"--pti-background":e.background,"--pti-background-hover":e.backgroundHover,"--pti-border":e.border,"--pti-border-focus":e.borderFocus,"--pti-ring":e.ring,"--pti-text":e.text,"--pti-placeholder":e.placeholder,"--pti-muted-text":e.mutedText,"--pti-tag-background":e.tagBackground,"--pti-tag-border":e.tagBorder,"--pti-tag-text":e.tagText,"--pti-tag-hover-background":e.tagHoverBackground,"--pti-tag-hover-border":e.tagHoverBorder,"--pti-remove-text":e.removeText,"--pti-remove-hover-background":e.removeHoverBackground,"--pti-remove-hover-text":e.removeHoverText,"--pti-radius":e.radius,"--pti-tag-radius":e.tagRadius,"--pti-shadow":e.shadow,"--pti-focus-shadow":e.focusShadow}}var a={default:i(r.default),light:i(r.light),dark:i(r.dark),cupcake:i(r.cupcake),emerald:i(r.emerald),corporate:i(r.corporate),retro:i(r.retro),dracula:i(r.dracula)};function o(e){return(0,t.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,"aria-hidden":`true`,...e,children:[(0,t.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`M20.59 13.41 13.41 20.59a2 2 0 0 1-2.82 0L3.41 13.41a2 2 0 0 1 0-2.82L10.59 3a2 2 0 0 1 1.41-.59H19a2 2 0 0 1 2 2v7a2 2 0 0 1-.59 1.41Z`}),(0,t.jsx)(`circle`,{cx:`16`,cy:`8`,r:`1`,fill:`currentColor`,stroke:`none`})]})}function s(e){return(0,t.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,"aria-hidden":`true`,...e,children:(0,t.jsx)(`path`,{strokeLinecap:`round`,d:`M6 6 18 18M18 6 6 18`})})}function c(e){return(0,t.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.8`,"aria-hidden":`true`,...e,children:(0,t.jsx)(`path`,{strokeLinecap:`round`,strokeLinejoin:`round`,d:`m6 6 12 12M18 6 6 18`})})}var l=(0,e.memo)(function({value:e,onChange:n,tagIcon:r,removeIcon:i,onTagRemove:a,removeButtonProps:c,disabled:l=!1,readOnly:u=!1}){let d=t=>{let r=e[t];r!==void 0&&(n([...e.slice(0,t),...e.slice(t+1)]),a?.(r,t))};return(0,t.jsx)(t.Fragment,{children:e.map((e,n)=>(0,t.jsxs)(`span`,{className:`taglite-tag`,children:[r??(0,t.jsx)(o,{className:`taglite-tag-icon`}),(0,t.jsx)(`span`,{className:`taglite-tag-label`,children:e}),(0,t.jsx)(`button`,{type:`button`,"aria-label":c?.[`aria-label`]??`Remove tag ${e}`,...c,disabled:l||u,onClick:e=>{e.stopPropagation(),d(n)},className:`taglite-remove-button ${c?.className??``}`,children:i??(0,t.jsx)(s,{className:`taglite-remove-icon`})})]},`${e}-${n}`))})}),u=[`Enter`,`,`],d={Enter:`
|
|
2
|
+
`,Tab:` `,Space:` `},f=e=>e.replace(/[.*+?^${}()|[\]\\-]/g,`\\$&`),p=e=>{let t=new Set([`,`,`
|
|
3
|
+
`,`\r`]);for(let n of e){if(n.length===1){t.add(n);continue}let e=d[n];e&&t.add(e)}return[...t]},m=(e,t)=>{let n=p(t).map(f).join(`|`),r=new RegExp(n);return{tags:e.split(r).map(e=>e.trim()).filter(Boolean),hasSeparator:r.test(e)}},h=(0,e.forwardRef)(function({className:n=``,placeholder:r=`Add a new tag...`,hintText:i=`Press Enter to add a tag`,value:o,onChange:s,direction:d=`ltr`,theme:f=`light`,tagIcon:p,removeIcon:h,clearIcon:g,separators:_=u,maxTags:v,allowDuplicates:y=!1,normalizeTag:b,validateTag:x,onInvalidTag:S,onTagAdd:C,onTagRemove:w,acceptOnBlur:T=!1,clearable:E=!1,onClear:D,removeButtonProps:O,disabled:k=!1,readOnly:A=!1,onKeyDown:j,onBlur:M,onPaste:N,...P},F){let[I,L]=(0,e.useState)(``),R=(0,e.useRef)(null),z=v!==void 0&&v<=o.length,B=new Set(_),V=e=>{R.current=e,typeof F==`function`?F(e):F&&(F.current=e)},H=e=>(b?b(e):e).trim(),U=(e,t=!0)=>{if(k||A||z)return t&&L(``),{tags:o,addedTags:[]};let n=[...o],r=[],i=b?new Set(o.map(e=>H(e))):new Set(o);for(let t of e){if(v!==void 0&&n.length>=v)break;let e=H(t);if(e){if(x){let t=x(e);if(t!==!0){S?.(e,typeof t==`string`?t:void 0);continue}}(y||!i.has(e))&&(n.push(e),r.push(e),i.add(e))}}if(r.length>0){let e=o.length;s(n);for(let t=0;t<r.length;t++)C?.(r[t],e+t)}return t&&L(``),{tags:n,addedTags:r}},W=e=>{L(e.target.value)},G=e=>{if(!k&&!A&&B.has(e.key)){e.preventDefault(),U([I]),j?.(e);return}if(!k&&!A&&e.key===`Backspace`&&!I&&o.length>0){let t=o.length-1,n=o[t];e.preventDefault(),s(o.slice(0,-1)),w?.(n,t),j?.(e);return}j?.(e)},K=e=>{if(!k&&!A){let{tags:t,hasSeparator:n}=m(e.clipboardData.getData(`text`),_);n&&(e.preventDefault(),U(t))}N?.(e)},q=e=>{T&&!k&&!A&&I.trim()&&U([I]),M?.(e)},J=()=>{k||A||o.length===0||(s([]),D?.(),L(``),R.current?.focus())};return(0,t.jsxs)(`div`,{dir:d,style:a[f],className:`taglite-root ${k?`taglite-root--disabled`:``} ${n}`,onClick:()=>{k||R.current?.focus()},children:[(0,t.jsx)(`span`,{"aria-hidden":`true`,className:`taglite-liquid-light`,style:{transform:`translate(-50%, -50%)`}}),(0,t.jsxs)(`div`,{className:`taglite-content`,children:[(0,t.jsx)(l,{value:o,onChange:s,tagIcon:p,removeIcon:h,onTagRemove:w,removeButtonProps:O,disabled:k,readOnly:A}),(0,t.jsx)(`input`,{ref:V,type:`text`,value:I,placeholder:r,disabled:k,readOnly:A,onChange:W,onKeyDown:G,onPaste:K,onBlur:q,className:`taglite-input`,...P})]}),E&&o.length>0&&(0,t.jsx)(`button`,{type:`button`,"aria-label":`Clear all tags`,disabled:k||A,onClick:e=>{e.stopPropagation(),J()},className:`taglite-clear-button`,children:g??(0,t.jsx)(c,{className:`taglite-clear-icon`})}),(0,t.jsx)(`div`,{className:`taglite-hint`,children:i})]})});exports.SimpleTagInput=h;
|
package/dist/taglite.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.taglite-root,.taglite-root *,.taglite-root :before,.taglite-root :after{box-sizing:border-box}.taglite-root{border:1px solid var(--pti-border);border-radius:var(--pti-radius);background:var(--pti-background);width:100%;min-height:3.5rem;color:var(--pti-text);box-shadow:var(--pti-shadow);padding:.625rem .75rem;transition:all .5s;position:relative;overflow:hidden}.taglite-root:focus-within{border-color:var(--pti-border-focus);box-shadow:var(--pti-focus-shadow)}.taglite-root--disabled{cursor:not-allowed;opacity:.6}.taglite-liquid-light{pointer-events:none;background:radial-gradient(circle, var(--pti-ring), transparent 65%);opacity:0;border-radius:9999px;width:100%;height:300%;transition:all .7s ease-out;position:absolute;top:-100%;left:-50%;transform:translate(-50%,-50%)}.taglite-root:focus-within .taglite-liquid-light{opacity:.2;top:50%;left:50%;scale:1}.taglite-content{flex-wrap:wrap;align-items:center;gap:.5rem;min-height:2rem;display:flex;position:relative}.taglite-tag{border:1px solid var(--pti-tag-border);border-radius:var(--pti-tag-radius);background:var(--pti-tag-background);color:var(--pti-tag-text);align-items:center;gap:.375rem;padding:.375rem .625rem;font-size:.75rem;font-weight:500;line-height:1rem;transition:color .3s ease-out,background-color .3s ease-out,border-color .3s ease-out;display:inline-flex}.taglite-tag:hover{border-color:var(--pti-tag-hover-border);background:var(--pti-tag-hover-background)}.taglite-tag-icon,.taglite-remove-icon{width:.75rem;height:.75rem}.taglite-tag-label{text-overflow:ellipsis;white-space:nowrap;max-width:10rem;overflow:hidden}.taglite-remove-button,.taglite-clear-button{cursor:pointer;color:inherit;font:inherit;background:0 0;border:0;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:color .2s,background-color .2s;display:flex}.taglite-remove-button{border-radius:9999px;width:1rem;height:1rem}.taglite-remove-button:hover{background:var(--pti-remove-hover-background)}.taglite-remove-button:disabled,.taglite-clear-button:disabled{pointer-events:none;cursor:not-allowed;opacity:.5}.taglite-input{min-width:8.75rem;color:var(--pti-text);font:inherit;background:0 0;border:0;outline:none;flex:1;padding:.375rem .25rem;font-size:.875rem;line-height:1.25rem;transition:color .3s}.taglite-input::placeholder{color:var(--pti-placeholder)}.taglite-input:disabled{cursor:not-allowed}.taglite-clear-button{inset-inline-end:.75rem;width:1.5rem;height:1.5rem;color:var(--pti-remove-text);border-radius:9999px;position:absolute;top:50%;transform:translateY(-50%)}.taglite-clear-button:hover{background:var(--pti-remove-hover-background);color:var(--pti-remove-hover-text)}.taglite-clear-icon{width:.875rem;height:.875rem}.taglite-hint{pointer-events:none;color:var(--pti-muted-text);opacity:0;margin-top:.375rem;padding:0 .25rem;font-size:10px;transition:opacity .3s,transform .3s;position:relative;transform:translateY(.25rem)}.taglite-root:focus-within .taglite-hint{opacity:1;transform:translateY(0)}
|
|
2
|
+
/*$vite$:1*/
|
package/dist/taglite.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { forwardRef as e, memo as t, useRef as n, useState as r } from "react";
|
|
2
|
+
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
3
|
+
//#region src/components/SimpleTagInput/SimpleTagInput.tsx
|
|
4
|
+
var s = {
|
|
5
|
+
background: "#ffffff",
|
|
6
|
+
backgroundHover: "#ffffff",
|
|
7
|
+
border: "#d1d5db",
|
|
8
|
+
borderFocus: "#111827",
|
|
9
|
+
ring: "rgba(17, 24, 39, 0.10)",
|
|
10
|
+
text: "#111827",
|
|
11
|
+
placeholder: "#9ca3af",
|
|
12
|
+
mutedText: "#9ca3af",
|
|
13
|
+
tagBackground: "#f3f4f6",
|
|
14
|
+
tagBorder: "#e5e7eb",
|
|
15
|
+
tagText: "#374151",
|
|
16
|
+
tagHoverBackground: "#e5e7eb",
|
|
17
|
+
tagHoverBorder: "#d1d5db",
|
|
18
|
+
removeText: "#6b7280",
|
|
19
|
+
removeHoverBackground: "#e5e7eb",
|
|
20
|
+
removeHoverText: "#111827",
|
|
21
|
+
radius: "1rem",
|
|
22
|
+
tagRadius: "1rem",
|
|
23
|
+
shadow: "0 1px 2px rgba(0, 0, 0, 0.03)",
|
|
24
|
+
focusShadow: "0 0 0 3px rgba(17, 24, 39, 0.10)"
|
|
25
|
+
}, c = {
|
|
26
|
+
default: s,
|
|
27
|
+
light: s,
|
|
28
|
+
dark: {
|
|
29
|
+
background: "#111827",
|
|
30
|
+
backgroundHover: "#111827",
|
|
31
|
+
border: "#374151",
|
|
32
|
+
borderFocus: "#f9fafb",
|
|
33
|
+
ring: "rgba(249, 250, 251, 0.10)",
|
|
34
|
+
text: "#f9fafb",
|
|
35
|
+
placeholder: "#6b7280",
|
|
36
|
+
mutedText: "#6b7280",
|
|
37
|
+
tagBackground: "#1f2937",
|
|
38
|
+
tagBorder: "#374151",
|
|
39
|
+
tagText: "#e5e7eb",
|
|
40
|
+
tagHoverBackground: "#374151",
|
|
41
|
+
tagHoverBorder: "#4b5563",
|
|
42
|
+
removeText: "#9ca3af",
|
|
43
|
+
removeHoverBackground: "#374151",
|
|
44
|
+
removeHoverText: "#f9fafb",
|
|
45
|
+
radius: "1rem",
|
|
46
|
+
tagRadius: "1rem",
|
|
47
|
+
shadow: "0 1px 2px rgba(0, 0, 0, 0.30)",
|
|
48
|
+
focusShadow: "0 0 0 3px rgba(249, 250, 251, 0.10)"
|
|
49
|
+
},
|
|
50
|
+
cupcake: {
|
|
51
|
+
background: "#fff7f3",
|
|
52
|
+
backgroundHover: "#fffaf7",
|
|
53
|
+
border: "#f0c8c0",
|
|
54
|
+
borderFocus: "#e6a89c",
|
|
55
|
+
ring: "rgba(230, 168, 156, 0.20)",
|
|
56
|
+
text: "#5c4b51",
|
|
57
|
+
placeholder: "#a9959c",
|
|
58
|
+
mutedText: "#a9959c",
|
|
59
|
+
tagBackground: "#f9dfe5",
|
|
60
|
+
tagBorder: "#efc4ce",
|
|
61
|
+
tagText: "#7b4f5b",
|
|
62
|
+
tagHoverBackground: "#f6d2da",
|
|
63
|
+
tagHoverBorder: "#e8b4c0",
|
|
64
|
+
removeText: "#a86b78",
|
|
65
|
+
removeHoverBackground: "#efc4ce",
|
|
66
|
+
removeHoverText: "#693f4b",
|
|
67
|
+
radius: "1rem",
|
|
68
|
+
tagRadius: "1rem",
|
|
69
|
+
shadow: "0 2px 8px rgba(92, 75, 81, 0.06)",
|
|
70
|
+
focusShadow: "0 0 0 3px rgba(230, 168, 156, 0.20)"
|
|
71
|
+
},
|
|
72
|
+
emerald: {
|
|
73
|
+
background: "#ffffff",
|
|
74
|
+
backgroundHover: "#fafffd",
|
|
75
|
+
border: "#a7d8c5",
|
|
76
|
+
borderFocus: "#10b981",
|
|
77
|
+
ring: "rgba(16, 185, 129, 0.14)",
|
|
78
|
+
text: "#163c31",
|
|
79
|
+
placeholder: "#7ca396",
|
|
80
|
+
mutedText: "#7ca396",
|
|
81
|
+
tagBackground: "#dff7ec",
|
|
82
|
+
tagBorder: "#b8ead4",
|
|
83
|
+
tagText: "#087f5b",
|
|
84
|
+
tagHoverBackground: "#cef1e1",
|
|
85
|
+
tagHoverBorder: "#9fdfc5",
|
|
86
|
+
removeText: "#2f9678",
|
|
87
|
+
removeHoverBackground: "#b8ead4",
|
|
88
|
+
removeHoverText: "#087f5b",
|
|
89
|
+
radius: "1rem",
|
|
90
|
+
tagRadius: "1rem",
|
|
91
|
+
shadow: "0 1px 3px rgba(16, 185, 129, 0.08)",
|
|
92
|
+
focusShadow: "0 0 0 3px rgba(16, 185, 129, 0.14)"
|
|
93
|
+
},
|
|
94
|
+
corporate: {
|
|
95
|
+
background: "#ffffff",
|
|
96
|
+
backgroundHover: "#ffffff",
|
|
97
|
+
border: "#cbd5e1",
|
|
98
|
+
borderFocus: "#1d4ed8",
|
|
99
|
+
ring: "rgba(29, 78, 216, 0.12)",
|
|
100
|
+
text: "#0f172a",
|
|
101
|
+
placeholder: "#94a3b8",
|
|
102
|
+
mutedText: "#94a3b8",
|
|
103
|
+
tagBackground: "#eff6ff",
|
|
104
|
+
tagBorder: "#bfdbfe",
|
|
105
|
+
tagText: "#1d4ed8",
|
|
106
|
+
tagHoverBackground: "#dbeafe",
|
|
107
|
+
tagHoverBorder: "#93c5fd",
|
|
108
|
+
removeText: "#64748b",
|
|
109
|
+
removeHoverBackground: "#dbeafe",
|
|
110
|
+
removeHoverText: "#1d4ed8",
|
|
111
|
+
radius: "1rem",
|
|
112
|
+
tagRadius: "1rem",
|
|
113
|
+
shadow: "0 1px 2px rgba(15, 23, 42, 0.04)",
|
|
114
|
+
focusShadow: "0 0 0 3px rgba(29, 78, 216, 0.12)"
|
|
115
|
+
},
|
|
116
|
+
retro: {
|
|
117
|
+
background: "#fdf6e3",
|
|
118
|
+
backgroundHover: "#fffaf0",
|
|
119
|
+
border: "#b8a27a",
|
|
120
|
+
borderFocus: "#9f1239",
|
|
121
|
+
ring: "rgba(159, 18, 57, 0.14)",
|
|
122
|
+
text: "#3f2f23",
|
|
123
|
+
placeholder: "#9b8b73",
|
|
124
|
+
mutedText: "#9b8b73",
|
|
125
|
+
tagBackground: "#f4d8a8",
|
|
126
|
+
tagBorder: "#d9b77a",
|
|
127
|
+
tagText: "#5c3b16",
|
|
128
|
+
tagHoverBackground: "#efd09a",
|
|
129
|
+
tagHoverBorder: "#cda967",
|
|
130
|
+
removeText: "#8b5e34",
|
|
131
|
+
removeHoverBackground: "#e7bf7e",
|
|
132
|
+
removeHoverText: "#5c3b16",
|
|
133
|
+
radius: "1rem",
|
|
134
|
+
tagRadius: "1rem",
|
|
135
|
+
shadow: "2px 2px 0 rgba(63, 47, 35, 0.12)",
|
|
136
|
+
focusShadow: "0 0 0 3px rgba(159, 18, 57, 0.14)"
|
|
137
|
+
},
|
|
138
|
+
dracula: {
|
|
139
|
+
background: "#282a36",
|
|
140
|
+
backgroundHover: "#2d2f3b",
|
|
141
|
+
border: "#44475a",
|
|
142
|
+
borderFocus: "#bd93f9",
|
|
143
|
+
ring: "rgba(189, 147, 249, 0.18)",
|
|
144
|
+
text: "#f8f8f2",
|
|
145
|
+
placeholder: "#6272a4",
|
|
146
|
+
mutedText: "#6272a4",
|
|
147
|
+
tagBackground: "#44475a",
|
|
148
|
+
tagBorder: "#6272a4",
|
|
149
|
+
tagText: "#f8f8f2",
|
|
150
|
+
tagHoverBackground: "#4f5266",
|
|
151
|
+
tagHoverBorder: "#bd93f9",
|
|
152
|
+
removeText: "#bd93f9",
|
|
153
|
+
removeHoverBackground: "#6272a4",
|
|
154
|
+
removeHoverText: "#f8f8f2",
|
|
155
|
+
radius: "1rem",
|
|
156
|
+
tagRadius: "1rem",
|
|
157
|
+
shadow: "0 2px 8px rgba(0, 0, 0, 0.30)",
|
|
158
|
+
focusShadow: "0 0 0 3px rgba(189, 147, 249, 0.18)"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function l(e) {
|
|
162
|
+
return {
|
|
163
|
+
"--pti-background": e.background,
|
|
164
|
+
"--pti-background-hover": e.backgroundHover,
|
|
165
|
+
"--pti-border": e.border,
|
|
166
|
+
"--pti-border-focus": e.borderFocus,
|
|
167
|
+
"--pti-ring": e.ring,
|
|
168
|
+
"--pti-text": e.text,
|
|
169
|
+
"--pti-placeholder": e.placeholder,
|
|
170
|
+
"--pti-muted-text": e.mutedText,
|
|
171
|
+
"--pti-tag-background": e.tagBackground,
|
|
172
|
+
"--pti-tag-border": e.tagBorder,
|
|
173
|
+
"--pti-tag-text": e.tagText,
|
|
174
|
+
"--pti-tag-hover-background": e.tagHoverBackground,
|
|
175
|
+
"--pti-tag-hover-border": e.tagHoverBorder,
|
|
176
|
+
"--pti-remove-text": e.removeText,
|
|
177
|
+
"--pti-remove-hover-background": e.removeHoverBackground,
|
|
178
|
+
"--pti-remove-hover-text": e.removeHoverText,
|
|
179
|
+
"--pti-radius": e.radius,
|
|
180
|
+
"--pti-tag-radius": e.tagRadius,
|
|
181
|
+
"--pti-shadow": e.shadow,
|
|
182
|
+
"--pti-focus-shadow": e.focusShadow
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var u = {
|
|
186
|
+
default: l(c.default),
|
|
187
|
+
light: l(c.light),
|
|
188
|
+
dark: l(c.dark),
|
|
189
|
+
cupcake: l(c.cupcake),
|
|
190
|
+
emerald: l(c.emerald),
|
|
191
|
+
corporate: l(c.corporate),
|
|
192
|
+
retro: l(c.retro),
|
|
193
|
+
dracula: l(c.dracula)
|
|
194
|
+
};
|
|
195
|
+
function d(e) {
|
|
196
|
+
return /* @__PURE__ */ o("svg", {
|
|
197
|
+
viewBox: "0 0 24 24",
|
|
198
|
+
fill: "none",
|
|
199
|
+
stroke: "currentColor",
|
|
200
|
+
strokeWidth: "1.8",
|
|
201
|
+
"aria-hidden": "true",
|
|
202
|
+
...e,
|
|
203
|
+
children: [/* @__PURE__ */ a("path", {
|
|
204
|
+
strokeLinecap: "round",
|
|
205
|
+
strokeLinejoin: "round",
|
|
206
|
+
d: "M20.59 13.41 13.41 20.59a2 2 0 0 1-2.82 0L3.41 13.41a2 2 0 0 1 0-2.82L10.59 3a2 2 0 0 1 1.41-.59H19a2 2 0 0 1 2 2v7a2 2 0 0 1-.59 1.41Z"
|
|
207
|
+
}), /* @__PURE__ */ a("circle", {
|
|
208
|
+
cx: "16",
|
|
209
|
+
cy: "8",
|
|
210
|
+
r: "1",
|
|
211
|
+
fill: "currentColor",
|
|
212
|
+
stroke: "none"
|
|
213
|
+
})]
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function f(e) {
|
|
217
|
+
return /* @__PURE__ */ a("svg", {
|
|
218
|
+
viewBox: "0 0 24 24",
|
|
219
|
+
fill: "none",
|
|
220
|
+
stroke: "currentColor",
|
|
221
|
+
strokeWidth: "2",
|
|
222
|
+
"aria-hidden": "true",
|
|
223
|
+
...e,
|
|
224
|
+
children: /* @__PURE__ */ a("path", {
|
|
225
|
+
strokeLinecap: "round",
|
|
226
|
+
d: "M6 6 18 18M18 6 6 18"
|
|
227
|
+
})
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function p(e) {
|
|
231
|
+
return /* @__PURE__ */ a("svg", {
|
|
232
|
+
viewBox: "0 0 24 24",
|
|
233
|
+
fill: "none",
|
|
234
|
+
stroke: "currentColor",
|
|
235
|
+
strokeWidth: "1.8",
|
|
236
|
+
"aria-hidden": "true",
|
|
237
|
+
...e,
|
|
238
|
+
children: /* @__PURE__ */ a("path", {
|
|
239
|
+
strokeLinecap: "round",
|
|
240
|
+
strokeLinejoin: "round",
|
|
241
|
+
d: "m6 6 12 12M18 6 6 18"
|
|
242
|
+
})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
var m = t(function({ value: e, onChange: t, tagIcon: n, removeIcon: r, onTagRemove: s, removeButtonProps: c, disabled: l = !1, readOnly: u = !1 }) {
|
|
246
|
+
let p = (n) => {
|
|
247
|
+
let r = e[n];
|
|
248
|
+
r !== void 0 && (t([...e.slice(0, n), ...e.slice(n + 1)]), s?.(r, n));
|
|
249
|
+
};
|
|
250
|
+
return /* @__PURE__ */ a(i, { children: e.map((e, t) => /* @__PURE__ */ o("span", {
|
|
251
|
+
className: "taglite-tag",
|
|
252
|
+
children: [
|
|
253
|
+
n ?? /* @__PURE__ */ a(d, { className: "taglite-tag-icon" }),
|
|
254
|
+
/* @__PURE__ */ a("span", {
|
|
255
|
+
className: "taglite-tag-label",
|
|
256
|
+
children: e
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ a("button", {
|
|
259
|
+
type: "button",
|
|
260
|
+
"aria-label": c?.["aria-label"] ?? `Remove tag ${e}`,
|
|
261
|
+
...c,
|
|
262
|
+
disabled: l || u,
|
|
263
|
+
onClick: (e) => {
|
|
264
|
+
e.stopPropagation(), p(t);
|
|
265
|
+
},
|
|
266
|
+
className: `taglite-remove-button ${c?.className ?? ""}`,
|
|
267
|
+
children: r ?? /* @__PURE__ */ a(f, { className: "taglite-remove-icon" })
|
|
268
|
+
})
|
|
269
|
+
]
|
|
270
|
+
}, `${e}-${t}`)) });
|
|
271
|
+
}), h = ["Enter", ","], g = {
|
|
272
|
+
Enter: "\n",
|
|
273
|
+
Tab: " ",
|
|
274
|
+
Space: " "
|
|
275
|
+
}, _ = (e) => e.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&"), v = (e) => {
|
|
276
|
+
let t = /* @__PURE__ */ new Set([
|
|
277
|
+
",",
|
|
278
|
+
"\n",
|
|
279
|
+
"\r"
|
|
280
|
+
]);
|
|
281
|
+
for (let n of e) {
|
|
282
|
+
if (n.length === 1) {
|
|
283
|
+
t.add(n);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
let e = g[n];
|
|
287
|
+
e && t.add(e);
|
|
288
|
+
}
|
|
289
|
+
return [...t];
|
|
290
|
+
}, y = (e, t) => {
|
|
291
|
+
let n = v(t).map(_).join("|"), r = new RegExp(n);
|
|
292
|
+
return {
|
|
293
|
+
tags: e.split(r).map((e) => e.trim()).filter(Boolean),
|
|
294
|
+
hasSeparator: r.test(e)
|
|
295
|
+
};
|
|
296
|
+
}, b = e(function({ className: e = "", placeholder: t = "Add a new tag...", hintText: i = "Press Enter to add a tag", value: s, onChange: c, direction: l = "ltr", theme: d = "light", tagIcon: f, removeIcon: g, clearIcon: _, separators: v = h, maxTags: b, allowDuplicates: x = !1, normalizeTag: S, validateTag: C, onInvalidTag: w, onTagAdd: T, onTagRemove: E, acceptOnBlur: D = !1, clearable: O = !1, onClear: k, removeButtonProps: A, disabled: j = !1, readOnly: M = !1, onKeyDown: N, onBlur: P, onPaste: F, ...I }, L) {
|
|
297
|
+
let [R, z] = r(""), B = n(null), V = b !== void 0 && b <= s.length, H = new Set(v), U = (e) => {
|
|
298
|
+
B.current = e, typeof L == "function" ? L(e) : L && (L.current = e);
|
|
299
|
+
}, W = (e) => (S ? S(e) : e).trim(), G = (e, t = !0) => {
|
|
300
|
+
if (j || M || V) return t && z(""), {
|
|
301
|
+
tags: s,
|
|
302
|
+
addedTags: []
|
|
303
|
+
};
|
|
304
|
+
let n = [...s], r = [], i = S ? new Set(s.map((e) => W(e))) : new Set(s);
|
|
305
|
+
for (let t of e) {
|
|
306
|
+
if (b !== void 0 && n.length >= b) break;
|
|
307
|
+
let e = W(t);
|
|
308
|
+
if (e) {
|
|
309
|
+
if (C) {
|
|
310
|
+
let t = C(e);
|
|
311
|
+
if (t !== !0) {
|
|
312
|
+
w?.(e, typeof t == "string" ? t : void 0);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
(x || !i.has(e)) && (n.push(e), r.push(e), i.add(e));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (r.length > 0) {
|
|
320
|
+
let e = s.length;
|
|
321
|
+
c(n);
|
|
322
|
+
for (let t = 0; t < r.length; t++) T?.(r[t], e + t);
|
|
323
|
+
}
|
|
324
|
+
return t && z(""), {
|
|
325
|
+
tags: n,
|
|
326
|
+
addedTags: r
|
|
327
|
+
};
|
|
328
|
+
}, K = (e) => {
|
|
329
|
+
z(e.target.value);
|
|
330
|
+
}, q = (e) => {
|
|
331
|
+
if (!j && !M && H.has(e.key)) {
|
|
332
|
+
e.preventDefault(), G([R]), N?.(e);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!j && !M && e.key === "Backspace" && !R && s.length > 0) {
|
|
336
|
+
let t = s.length - 1, n = s[t];
|
|
337
|
+
e.preventDefault(), c(s.slice(0, -1)), E?.(n, t), N?.(e);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
N?.(e);
|
|
341
|
+
}, J = (e) => {
|
|
342
|
+
if (!j && !M) {
|
|
343
|
+
let { tags: t, hasSeparator: n } = y(e.clipboardData.getData("text"), v);
|
|
344
|
+
n && (e.preventDefault(), G(t));
|
|
345
|
+
}
|
|
346
|
+
F?.(e);
|
|
347
|
+
}, Y = (e) => {
|
|
348
|
+
D && !j && !M && R.trim() && G([R]), P?.(e);
|
|
349
|
+
}, X = () => {
|
|
350
|
+
j || M || s.length === 0 || (c([]), k?.(), z(""), B.current?.focus());
|
|
351
|
+
};
|
|
352
|
+
return /* @__PURE__ */ o("div", {
|
|
353
|
+
dir: l,
|
|
354
|
+
style: u[d],
|
|
355
|
+
className: `taglite-root ${j ? "taglite-root--disabled" : ""} ${e}`,
|
|
356
|
+
onClick: () => {
|
|
357
|
+
j || B.current?.focus();
|
|
358
|
+
},
|
|
359
|
+
children: [
|
|
360
|
+
/* @__PURE__ */ a("span", {
|
|
361
|
+
"aria-hidden": "true",
|
|
362
|
+
className: "taglite-liquid-light",
|
|
363
|
+
style: { transform: "translate(-50%, -50%)" }
|
|
364
|
+
}),
|
|
365
|
+
/* @__PURE__ */ o("div", {
|
|
366
|
+
className: "taglite-content",
|
|
367
|
+
children: [/* @__PURE__ */ a(m, {
|
|
368
|
+
value: s,
|
|
369
|
+
onChange: c,
|
|
370
|
+
tagIcon: f,
|
|
371
|
+
removeIcon: g,
|
|
372
|
+
onTagRemove: E,
|
|
373
|
+
removeButtonProps: A,
|
|
374
|
+
disabled: j,
|
|
375
|
+
readOnly: M
|
|
376
|
+
}), /* @__PURE__ */ a("input", {
|
|
377
|
+
ref: U,
|
|
378
|
+
type: "text",
|
|
379
|
+
value: R,
|
|
380
|
+
placeholder: t,
|
|
381
|
+
disabled: j,
|
|
382
|
+
readOnly: M,
|
|
383
|
+
onChange: K,
|
|
384
|
+
onKeyDown: q,
|
|
385
|
+
onPaste: J,
|
|
386
|
+
onBlur: Y,
|
|
387
|
+
className: "taglite-input",
|
|
388
|
+
...I
|
|
389
|
+
})]
|
|
390
|
+
}),
|
|
391
|
+
O && s.length > 0 && /* @__PURE__ */ a("button", {
|
|
392
|
+
type: "button",
|
|
393
|
+
"aria-label": "Clear all tags",
|
|
394
|
+
disabled: j || M,
|
|
395
|
+
onClick: (e) => {
|
|
396
|
+
e.stopPropagation(), X();
|
|
397
|
+
},
|
|
398
|
+
className: "taglite-clear-button",
|
|
399
|
+
children: _ ?? /* @__PURE__ */ a(p, { className: "taglite-clear-icon" })
|
|
400
|
+
}),
|
|
401
|
+
/* @__PURE__ */ a("div", {
|
|
402
|
+
className: "taglite-hint",
|
|
403
|
+
children: i
|
|
404
|
+
})
|
|
405
|
+
]
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
//#endregion
|
|
409
|
+
export { b as SimpleTagInput };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taglite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, dependency-free React tag input component.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"tag-input",
|
|
8
|
+
"tags-input",
|
|
9
|
+
"typescript",
|
|
10
|
+
"component"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/hossein-nj/taglite#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/hossein-nj/taglite.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/hossein-nj/taglite/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/taglite.cjs",
|
|
25
|
+
"module": "./dist/taglite.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/taglite.js",
|
|
31
|
+
"require": "./dist/taglite.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./style.css": "./dist/taglite.css",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": [
|
|
37
|
+
"**/*.css"
|
|
38
|
+
],
|
|
39
|
+
"type": "module",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "vite",
|
|
42
|
+
"build": "tsc -b && vite build",
|
|
43
|
+
"build:lib": "vite build --mode lib && tsc -p tsconfig.lib.json",
|
|
44
|
+
"lint": "eslint .",
|
|
45
|
+
"preview": "vite preview"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
52
|
+
"react": "^19.2.8",
|
|
53
|
+
"react-dom": "^19.2.8",
|
|
54
|
+
"tailwindcss": "^4.3.3",
|
|
55
|
+
"@eslint/js": "^10.0.1",
|
|
56
|
+
"@types/node": "^24.13.3",
|
|
57
|
+
"@types/react": "^19.2.18",
|
|
58
|
+
"@types/react-dom": "^19.2.7",
|
|
59
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
60
|
+
"eslint": "^10.10.0",
|
|
61
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
62
|
+
"eslint-plugin-react-refresh": "^0.5.6",
|
|
63
|
+
"globals": "^17.12.0",
|
|
64
|
+
"typescript": "~6.0.2",
|
|
65
|
+
"typescript-eslint": "^8.69.0",
|
|
66
|
+
"vite": "^8.3.0"
|
|
67
|
+
}
|
|
68
|
+
}
|