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 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
+ [![npm version](https://img.shields.io/npm/v/taglite?style=flat-square&color=cb3837)](https://www.npmjs.com/package/taglite)
8
+ [![npm downloads](https://img.shields.io/npm/dm/taglite?style=flat-square&color=blue)](https://www.npmjs.com/package/taglite)
9
+ [![React](https://img.shields.io/badge/React-%3E%3D18-61dafb?style=flat-square&logo=react&logoColor=20232a)](https://react.dev/)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green?style=flat-square)](./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>&gt;=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[]) =&gt; 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) =&gt; string</code> | — | Normalizes tags before validation |
189
+ | <code>validateTag</code> | <code>(tag: string) =&gt; boolean or string</code> | — | Validates tags |
190
+ | <code>onInvalidTag</code> | <code>(tag, reason?) =&gt; void</code> | — | Called for validation failures |
191
+ | <code>onTagAdd</code> | <code>(tag, index) =&gt; void</code> | — | Called after a tag is added |
192
+ | <code>onTagRemove</code> | <code>(tag, index) =&gt; 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>() =&gt; 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&lt;HTMLInputElement&gt;</code> | — | Ref to the native input |
203
+
204
+ <code>SimpleTagInput</code> also accepts standard <code>InputHTMLAttributes&lt;HTMLInputElement&gt;</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 =&gt; 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&lt;HTMLInputElement&gt;</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>&lt;input&gt;</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,3 @@
1
+ import type { SimpleTagInputProps } from './SimpleTagInput.types';
2
+ declare const _default: import("react").ForwardRefExoticComponent<SimpleTagInputProps & import("react").RefAttributes<HTMLInputElement>>;
3
+ export default _default;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export { default as SimpleTagInput } from './SimpleTagInput';
2
+ export { default as ProductTagsInput } from './SimpleTagInput';
3
+ export type { SimpleTagInputProps, SimpleTagInputTheme } from './SimpleTagInput.types';
@@ -0,0 +1,2 @@
1
+ export { SimpleTagInput } from './components/SimpleTagInput';
2
+ export type { SimpleTagInputProps, SimpleTagInputTheme } from './components/SimpleTagInput';
@@ -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;
@@ -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*/
@@ -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
+ }