@finsweet/webflow-apps-utils 1.0.50 → 1.0.51
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/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/tags/TagsInput.stories.d.ts +103 -0
- package/dist/ui/components/tags/TagsInput.stories.js +435 -0
- package/dist/ui/components/tags/TagsInput.svelte +478 -0
- package/dist/ui/components/tags/TagsInput.svelte.d.ts +4 -0
- package/dist/ui/components/tags/index.d.ts +2 -0
- package/dist/ui/components/tags/index.js +2 -0
- package/dist/ui/components/tags/types.d.ts +123 -0
- package/dist/ui/components/tags/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/sveltekit';
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
component: import("svelte").Component<import("./types.js").TagsInputProps, {}, "value">;
|
|
5
|
+
parameters: {
|
|
6
|
+
layout: string;
|
|
7
|
+
docs: {
|
|
8
|
+
description: {
|
|
9
|
+
component: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
args: {
|
|
14
|
+
width: string;
|
|
15
|
+
height: string;
|
|
16
|
+
};
|
|
17
|
+
tags: string[];
|
|
18
|
+
argTypes: {
|
|
19
|
+
value: {
|
|
20
|
+
control: "object";
|
|
21
|
+
description: string;
|
|
22
|
+
};
|
|
23
|
+
placeholder: {
|
|
24
|
+
control: "text";
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
disabled: {
|
|
28
|
+
control: "boolean";
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
loading: {
|
|
32
|
+
control: "boolean";
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
invalid: {
|
|
36
|
+
control: "boolean";
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
readonly: {
|
|
40
|
+
control: "boolean";
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
maxTags: {
|
|
44
|
+
control: "number";
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
minTags: {
|
|
48
|
+
control: "number";
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
maxTagLength: {
|
|
52
|
+
control: "number";
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
allowDuplicates: {
|
|
56
|
+
control: "boolean";
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
trimTags: {
|
|
60
|
+
control: "boolean";
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
width: {
|
|
64
|
+
control: "text";
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
height: {
|
|
68
|
+
control: "text";
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export default meta;
|
|
74
|
+
type Story = StoryObj<typeof meta>;
|
|
75
|
+
export declare const Default: Story;
|
|
76
|
+
export declare const WithInitialTags: Story;
|
|
77
|
+
export declare const WithPlaceholder: Story;
|
|
78
|
+
export declare const Disabled: Story;
|
|
79
|
+
export declare const Loading: Story;
|
|
80
|
+
export declare const Invalid: Story;
|
|
81
|
+
export declare const ReadOnly: Story;
|
|
82
|
+
export declare const MaxTags: Story;
|
|
83
|
+
export declare const MaxTagLength: Story;
|
|
84
|
+
export declare const NoDuplicates: Story;
|
|
85
|
+
export declare const AllowDuplicates: Story;
|
|
86
|
+
export declare const ErrorAlert: Story;
|
|
87
|
+
export declare const SuccessAlert: Story;
|
|
88
|
+
export declare const InfoAlert: Story;
|
|
89
|
+
export declare const WarningAlert: Story;
|
|
90
|
+
export declare const CustomWidth: Story;
|
|
91
|
+
export declare const CustomHeight: Story;
|
|
92
|
+
export declare const FullWidth: Story;
|
|
93
|
+
export declare const InteractiveExample: Story;
|
|
94
|
+
export declare const CustomValidation: Story;
|
|
95
|
+
export declare const EmailValidation: Story;
|
|
96
|
+
export declare const CategoryTags: Story;
|
|
97
|
+
export declare const SkillsTags: Story;
|
|
98
|
+
export declare const KeywordsTags: Story;
|
|
99
|
+
export declare const FormIntegration: Story;
|
|
100
|
+
export declare const ManyTags: Story;
|
|
101
|
+
export declare const LongTags: Story;
|
|
102
|
+
export declare const SpecialCharacters: Story;
|
|
103
|
+
export declare const UnicodeSupport: Story;
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import TagsInput from './TagsInput.svelte';
|
|
2
|
+
const meta = {
|
|
3
|
+
title: 'Ui/TagsInput',
|
|
4
|
+
component: TagsInput,
|
|
5
|
+
parameters: {
|
|
6
|
+
layout: 'centered',
|
|
7
|
+
docs: {
|
|
8
|
+
description: {
|
|
9
|
+
component: 'A versatile tags input component with support for various states, validation, and customization. Similar to shadcn tags input but styled for our design system.'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
width: '420px',
|
|
15
|
+
height: '70px'
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
argTypes: {
|
|
19
|
+
value: {
|
|
20
|
+
control: 'object',
|
|
21
|
+
description: 'Array of tag values'
|
|
22
|
+
},
|
|
23
|
+
placeholder: {
|
|
24
|
+
control: 'text',
|
|
25
|
+
description: 'Placeholder text when no tags and input is empty'
|
|
26
|
+
},
|
|
27
|
+
disabled: {
|
|
28
|
+
control: 'boolean',
|
|
29
|
+
description: 'Disables the entire component'
|
|
30
|
+
},
|
|
31
|
+
loading: {
|
|
32
|
+
control: 'boolean',
|
|
33
|
+
description: 'Shows loading state'
|
|
34
|
+
},
|
|
35
|
+
invalid: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Applies invalid styling'
|
|
38
|
+
},
|
|
39
|
+
readonly: {
|
|
40
|
+
control: 'boolean',
|
|
41
|
+
description: 'Makes tags read-only (no adding/removing)'
|
|
42
|
+
},
|
|
43
|
+
maxTags: {
|
|
44
|
+
control: 'number',
|
|
45
|
+
description: 'Maximum number of tags allowed'
|
|
46
|
+
},
|
|
47
|
+
minTags: {
|
|
48
|
+
control: 'number',
|
|
49
|
+
description: 'Minimum number of tags required'
|
|
50
|
+
},
|
|
51
|
+
maxTagLength: {
|
|
52
|
+
control: 'number',
|
|
53
|
+
description: 'Maximum length of each tag'
|
|
54
|
+
},
|
|
55
|
+
allowDuplicates: {
|
|
56
|
+
control: 'boolean',
|
|
57
|
+
description: 'Whether to allow duplicate tags'
|
|
58
|
+
},
|
|
59
|
+
trimTags: {
|
|
60
|
+
control: 'boolean',
|
|
61
|
+
description: 'Whether to trim whitespace from tags'
|
|
62
|
+
},
|
|
63
|
+
width: {
|
|
64
|
+
control: 'text',
|
|
65
|
+
description: 'Custom width for the component'
|
|
66
|
+
},
|
|
67
|
+
height: {
|
|
68
|
+
control: 'text',
|
|
69
|
+
description: 'Custom height for the component'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export default meta;
|
|
74
|
+
// Basic stories
|
|
75
|
+
export const Default = {
|
|
76
|
+
args: {
|
|
77
|
+
placeholder: 'Add tags...'
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export const WithInitialTags = {
|
|
81
|
+
args: {
|
|
82
|
+
value: ['JavaScript', 'TypeScript', 'Svelte'],
|
|
83
|
+
placeholder: 'Add more tags...'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export const WithPlaceholder = {
|
|
87
|
+
args: {
|
|
88
|
+
placeholder: 'Type and press Enter to add tags...'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// States
|
|
92
|
+
export const Disabled = {
|
|
93
|
+
args: {
|
|
94
|
+
value: ['Disabled', 'Tag'],
|
|
95
|
+
disabled: true
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
export const Loading = {
|
|
99
|
+
args: {
|
|
100
|
+
value: ['Loading', 'Tags'],
|
|
101
|
+
loading: true
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
export const Invalid = {
|
|
105
|
+
args: {
|
|
106
|
+
value: ['Invalid', 'Input'],
|
|
107
|
+
invalid: true
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
export const ReadOnly = {
|
|
111
|
+
args: {
|
|
112
|
+
value: ['Read', 'Only', 'Tags'],
|
|
113
|
+
readonly: true
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Constraints
|
|
117
|
+
export const MaxTags = {
|
|
118
|
+
args: {
|
|
119
|
+
value: ['Tag 1', 'Tag 2', 'Tag 3'],
|
|
120
|
+
maxTags: 3,
|
|
121
|
+
placeholder: 'Max 3 tags reached'
|
|
122
|
+
},
|
|
123
|
+
parameters: {
|
|
124
|
+
docs: {
|
|
125
|
+
description: {
|
|
126
|
+
story: 'Maximum of 3 tags allowed. Input is hidden when limit is reached.'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
export const MaxTagLength = {
|
|
132
|
+
args: {
|
|
133
|
+
value: ['short'],
|
|
134
|
+
maxTagLength: 10,
|
|
135
|
+
placeholder: 'Max 10 chars per tag...'
|
|
136
|
+
},
|
|
137
|
+
parameters: {
|
|
138
|
+
docs: {
|
|
139
|
+
description: {
|
|
140
|
+
story: 'Tags cannot exceed 10 characters.'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
export const NoDuplicates = {
|
|
146
|
+
args: {
|
|
147
|
+
value: ['unique', 'tags'],
|
|
148
|
+
allowDuplicates: false,
|
|
149
|
+
placeholder: 'Try adding "unique" again...'
|
|
150
|
+
},
|
|
151
|
+
parameters: {
|
|
152
|
+
docs: {
|
|
153
|
+
description: {
|
|
154
|
+
story: 'Duplicate tags are not allowed (default behavior).'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export const AllowDuplicates = {
|
|
160
|
+
args: {
|
|
161
|
+
value: ['duplicate', 'duplicate'],
|
|
162
|
+
allowDuplicates: true,
|
|
163
|
+
placeholder: 'Duplicates allowed here...'
|
|
164
|
+
},
|
|
165
|
+
parameters: {
|
|
166
|
+
docs: {
|
|
167
|
+
description: {
|
|
168
|
+
story: 'Duplicate tags are allowed when allowDuplicates is true.'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
// Alert states
|
|
174
|
+
export const ErrorAlert = {
|
|
175
|
+
args: {
|
|
176
|
+
value: ['Invalid'],
|
|
177
|
+
invalid: true,
|
|
178
|
+
alert: {
|
|
179
|
+
type: 'error',
|
|
180
|
+
message: 'Please add at least 3 tags'
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
parameters: {
|
|
184
|
+
docs: {
|
|
185
|
+
description: {
|
|
186
|
+
story: 'Tags input with error state. Hover to see the error tooltip.'
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
export const SuccessAlert = {
|
|
192
|
+
args: {
|
|
193
|
+
value: ['Valid', 'Tags', 'Added'],
|
|
194
|
+
alert: {
|
|
195
|
+
type: 'success',
|
|
196
|
+
message: 'Tags are valid!'
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
parameters: {
|
|
200
|
+
docs: {
|
|
201
|
+
description: {
|
|
202
|
+
story: 'Tags input with success state. Hover to see the success tooltip.'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
export const InfoAlert = {
|
|
208
|
+
args: {
|
|
209
|
+
value: ['Info'],
|
|
210
|
+
alert: {
|
|
211
|
+
type: 'info',
|
|
212
|
+
message: 'Add tags separated by comma or Enter'
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
parameters: {
|
|
216
|
+
docs: {
|
|
217
|
+
description: {
|
|
218
|
+
story: 'Tags input with info state. Hover to see the info tooltip.'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
export const WarningAlert = {
|
|
224
|
+
args: {
|
|
225
|
+
value: ['Warning', 'Tag'],
|
|
226
|
+
alert: {
|
|
227
|
+
type: 'warning',
|
|
228
|
+
message: 'Some tags may need review'
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
parameters: {
|
|
232
|
+
docs: {
|
|
233
|
+
description: {
|
|
234
|
+
story: 'Tags input with warning state. Hover to see the warning tooltip.'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// Sizing
|
|
240
|
+
export const CustomWidth = {
|
|
241
|
+
args: {
|
|
242
|
+
value: ['Custom', 'Width'],
|
|
243
|
+
width: '400px',
|
|
244
|
+
placeholder: 'Wide input...'
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
export const CustomHeight = {
|
|
248
|
+
args: {
|
|
249
|
+
value: ['Custom', 'Height', 'With', 'Multiple', 'Tags'],
|
|
250
|
+
height: '80px',
|
|
251
|
+
placeholder: 'Taller input...'
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
export const FullWidth = {
|
|
255
|
+
args: {
|
|
256
|
+
value: ['Full'],
|
|
257
|
+
width: '100%',
|
|
258
|
+
placeholder: 'Full width input...'
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
// Interactive examples
|
|
262
|
+
export const InteractiveExample = {
|
|
263
|
+
args: {
|
|
264
|
+
value: ['React', 'Vue'],
|
|
265
|
+
placeholder: 'Add a framework...',
|
|
266
|
+
onValueChange: (tags) => console.log('Tags changed:', tags),
|
|
267
|
+
onTagAdd: (tag) => console.log('Tag added:', tag),
|
|
268
|
+
onTagRemove: (tag, index) => console.log('Tag removed:', tag, 'at index', index),
|
|
269
|
+
onInvalidTag: (tag, reason) => console.log('Invalid tag:', tag, 'Reason:', reason)
|
|
270
|
+
},
|
|
271
|
+
parameters: {
|
|
272
|
+
docs: {
|
|
273
|
+
description: {
|
|
274
|
+
story: 'Interactive example with all event handlers. Check the console for events.'
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
// Validation examples
|
|
280
|
+
export const CustomValidation = {
|
|
281
|
+
args: {
|
|
282
|
+
value: ['#valid'],
|
|
283
|
+
placeholder: 'Tags must start with #...',
|
|
284
|
+
validateTag: (tag) => {
|
|
285
|
+
if (!tag.startsWith('#')) {
|
|
286
|
+
return 'Tags must start with #';
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
},
|
|
290
|
+
onInvalidTag: (tag, reason) => console.log('Invalid:', tag, reason)
|
|
291
|
+
},
|
|
292
|
+
parameters: {
|
|
293
|
+
docs: {
|
|
294
|
+
description: {
|
|
295
|
+
story: 'Custom validation requiring tags to start with #. Try adding a tag without #.'
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
export const EmailValidation = {
|
|
301
|
+
args: {
|
|
302
|
+
value: ['user@example.com'],
|
|
303
|
+
placeholder: 'Add email addresses...',
|
|
304
|
+
validateTag: (tag) => {
|
|
305
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
306
|
+
if (!emailRegex.test(tag)) {
|
|
307
|
+
return 'Please enter a valid email';
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
},
|
|
311
|
+
onInvalidTag: (tag, reason) => console.log('Invalid:', tag, reason)
|
|
312
|
+
},
|
|
313
|
+
parameters: {
|
|
314
|
+
docs: {
|
|
315
|
+
description: {
|
|
316
|
+
story: 'Email validation example. Only valid email addresses are accepted.'
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
// Real-world examples
|
|
322
|
+
export const CategoryTags = {
|
|
323
|
+
args: {
|
|
324
|
+
value: ['Technology', 'Design', 'Marketing'],
|
|
325
|
+
placeholder: 'Add categories...',
|
|
326
|
+
maxTags: 5
|
|
327
|
+
},
|
|
328
|
+
parameters: {
|
|
329
|
+
docs: {
|
|
330
|
+
description: {
|
|
331
|
+
story: 'Category tagging with a maximum of 5 tags.'
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
export const SkillsTags = {
|
|
337
|
+
args: {
|
|
338
|
+
value: ['JavaScript', 'React', 'Node.js', 'CSS'],
|
|
339
|
+
placeholder: 'Add your skills...',
|
|
340
|
+
maxTagLength: 20
|
|
341
|
+
},
|
|
342
|
+
parameters: {
|
|
343
|
+
docs: {
|
|
344
|
+
description: {
|
|
345
|
+
story: 'Skills input with max tag length of 20 characters.'
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
export const KeywordsTags = {
|
|
351
|
+
args: {
|
|
352
|
+
value: ['seo', 'marketing', 'content'],
|
|
353
|
+
placeholder: 'Add keywords for SEO...',
|
|
354
|
+
maxTags: 10,
|
|
355
|
+
onValueChange: (tags) => console.log('Keywords:', tags)
|
|
356
|
+
},
|
|
357
|
+
parameters: {
|
|
358
|
+
docs: {
|
|
359
|
+
description: {
|
|
360
|
+
story: 'SEO keywords input with up to 10 tags.'
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
export const FormIntegration = {
|
|
366
|
+
args: {
|
|
367
|
+
id: 'form-tags',
|
|
368
|
+
value: ['tag1'],
|
|
369
|
+
placeholder: 'Add tags for form...',
|
|
370
|
+
alert: {
|
|
371
|
+
type: 'info',
|
|
372
|
+
message: 'Tags will be submitted with the form'
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
parameters: {
|
|
376
|
+
docs: {
|
|
377
|
+
description: {
|
|
378
|
+
story: 'Example showing tags input in a form context. Compatible with Zod validation schemas.'
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
// Edge cases
|
|
384
|
+
export const ManyTags = {
|
|
385
|
+
args: {
|
|
386
|
+
value: Array.from({ length: 20 }, (_, i) => `Tag ${i + 1}`),
|
|
387
|
+
placeholder: 'Lots of tags...'
|
|
388
|
+
},
|
|
389
|
+
parameters: {
|
|
390
|
+
docs: {
|
|
391
|
+
description: {
|
|
392
|
+
story: 'Performance test with 20 tags.'
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
export const LongTags = {
|
|
398
|
+
args: {
|
|
399
|
+
value: ['This is a very long tag name', 'Another extremely long tag'],
|
|
400
|
+
placeholder: 'Long tags get truncated...'
|
|
401
|
+
},
|
|
402
|
+
parameters: {
|
|
403
|
+
docs: {
|
|
404
|
+
description: {
|
|
405
|
+
story: 'Long tags are truncated with ellipsis.'
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
export const SpecialCharacters = {
|
|
411
|
+
args: {
|
|
412
|
+
value: ['C++', 'C#', '.NET', '@angular', '#svelte'],
|
|
413
|
+
placeholder: 'Special characters work...'
|
|
414
|
+
},
|
|
415
|
+
parameters: {
|
|
416
|
+
docs: {
|
|
417
|
+
description: {
|
|
418
|
+
story: 'Tags with special characters are supported.'
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
export const UnicodeSupport = {
|
|
424
|
+
args: {
|
|
425
|
+
value: ['日本語', '中文', '한국어', '🎉', '✨'],
|
|
426
|
+
placeholder: 'Unicode support...'
|
|
427
|
+
},
|
|
428
|
+
parameters: {
|
|
429
|
+
docs: {
|
|
430
|
+
description: {
|
|
431
|
+
story: 'Full Unicode support including CJK characters and emoji.'
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TimesIcon } from '../../icons';
|
|
3
|
+
|
|
4
|
+
import Loader from '../Loader.svelte';
|
|
5
|
+
import { Tooltip } from '../tooltip';
|
|
6
|
+
import type { TagsInputProps } from './types.js';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
value = $bindable([]),
|
|
10
|
+
placeholder = 'Add tag...',
|
|
11
|
+
id = 'tags-input',
|
|
12
|
+
disabled = false,
|
|
13
|
+
loading = false,
|
|
14
|
+
invalid = false,
|
|
15
|
+
readonly = false,
|
|
16
|
+
alert = null,
|
|
17
|
+
maxTags = null,
|
|
18
|
+
minTags = null,
|
|
19
|
+
maxTagLength = null,
|
|
20
|
+
separatorKeys = ['Enter', ','],
|
|
21
|
+
allowDuplicates = false,
|
|
22
|
+
validateTag,
|
|
23
|
+
trimTags = true,
|
|
24
|
+
width = '100%',
|
|
25
|
+
height = 'auto',
|
|
26
|
+
class: className = '',
|
|
27
|
+
onValueChange,
|
|
28
|
+
onTagAdd,
|
|
29
|
+
onTagRemove,
|
|
30
|
+
onInvalidTag,
|
|
31
|
+
onfocus,
|
|
32
|
+
onblur,
|
|
33
|
+
onkeydown,
|
|
34
|
+
children,
|
|
35
|
+
...restProps
|
|
36
|
+
}: TagsInputProps = $props();
|
|
37
|
+
|
|
38
|
+
// Component state
|
|
39
|
+
let inputElement: HTMLInputElement | undefined = $state();
|
|
40
|
+
let inputValue = $state('');
|
|
41
|
+
let isFocused = $state(false);
|
|
42
|
+
|
|
43
|
+
// Derived states
|
|
44
|
+
let isDisabled = $derived(disabled || loading);
|
|
45
|
+
let canAddMore = $derived(maxTags === null || value.length < maxTags);
|
|
46
|
+
let showPlaceholder = $derived(value.length === 0 && !inputValue);
|
|
47
|
+
let hasAlert = $derived(alert?.message);
|
|
48
|
+
|
|
49
|
+
// Validation state
|
|
50
|
+
let isMinTagsInvalid = $derived.by(() => {
|
|
51
|
+
if (minTags === null) return false;
|
|
52
|
+
return value.length < minTags;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// CSS classes
|
|
56
|
+
let wrapperClasses = $derived(
|
|
57
|
+
`
|
|
58
|
+
tags-input-wrapper
|
|
59
|
+
${isDisabled ? 'disabled' : ''}
|
|
60
|
+
${invalid || hasAlert || isMinTagsInvalid ? 'invalid' : ''}
|
|
61
|
+
${isFocused ? 'focused' : ''}
|
|
62
|
+
${loading ? 'loading' : ''}
|
|
63
|
+
${className}
|
|
64
|
+
`
|
|
65
|
+
.trim()
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Focus the input element
|
|
71
|
+
*/
|
|
72
|
+
const focusInput = () => {
|
|
73
|
+
if (inputElement && !readonly && !isDisabled) {
|
|
74
|
+
inputElement.focus();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validates a tag before adding
|
|
80
|
+
*/
|
|
81
|
+
const validateTagValue = (tag: string): { valid: boolean; reason?: string } => {
|
|
82
|
+
const trimmedTag = trimTags ? tag.trim() : tag;
|
|
83
|
+
|
|
84
|
+
if (!trimmedTag) {
|
|
85
|
+
return { valid: false, reason: 'Tag cannot be empty' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (maxTagLength !== null && trimmedTag.length > maxTagLength) {
|
|
89
|
+
return { valid: false, reason: `Tag exceeds maximum length of ${maxTagLength}` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!allowDuplicates && value.includes(trimmedTag)) {
|
|
93
|
+
return { valid: false, reason: 'Duplicate tag' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!canAddMore) {
|
|
97
|
+
return { valid: false, reason: `Maximum of ${maxTags} tags allowed` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (validateTag) {
|
|
101
|
+
const customValidation = validateTag(trimmedTag);
|
|
102
|
+
if (customValidation === false) {
|
|
103
|
+
return { valid: false, reason: 'Invalid tag' };
|
|
104
|
+
}
|
|
105
|
+
if (typeof customValidation === 'string') {
|
|
106
|
+
return { valid: false, reason: customValidation };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Adds a new tag
|
|
115
|
+
*/
|
|
116
|
+
const addTag = (rawTag: string) => {
|
|
117
|
+
if (readonly || isDisabled) return;
|
|
118
|
+
|
|
119
|
+
const tag = trimTags ? rawTag.trim() : rawTag;
|
|
120
|
+
const validation = validateTagValue(tag);
|
|
121
|
+
|
|
122
|
+
if (!validation.valid) {
|
|
123
|
+
onInvalidTag?.(tag, validation.reason || 'Invalid tag');
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
inputValue = '';
|
|
128
|
+
value = [...value, tag];
|
|
129
|
+
|
|
130
|
+
onTagAdd?.(tag);
|
|
131
|
+
onValueChange?.(value);
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Removes a tag at the specified index
|
|
138
|
+
*/
|
|
139
|
+
const removeTag = (index: number) => {
|
|
140
|
+
if (readonly || isDisabled) return;
|
|
141
|
+
|
|
142
|
+
const removedTag = value[index];
|
|
143
|
+
value = value.filter((_, i) => i !== index);
|
|
144
|
+
|
|
145
|
+
onTagRemove?.(removedTag, index);
|
|
146
|
+
onValueChange?.(value);
|
|
147
|
+
|
|
148
|
+
// Focus back to input after removal
|
|
149
|
+
focusInput();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handles input changes
|
|
154
|
+
*/
|
|
155
|
+
const handleInput = (event: Event) => {
|
|
156
|
+
const target = event.target as HTMLInputElement;
|
|
157
|
+
const newValue = target.value;
|
|
158
|
+
|
|
159
|
+
// Check if a separator key was typed (e.g., comma)
|
|
160
|
+
for (const separator of separatorKeys) {
|
|
161
|
+
if (separator.length === 1 && newValue.includes(separator)) {
|
|
162
|
+
const parts = newValue.split(separator);
|
|
163
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
164
|
+
if (parts[i].trim()) {
|
|
165
|
+
addTag(parts[i]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
inputValue = parts[parts.length - 1];
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
inputValue = newValue;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handles keydown events
|
|
178
|
+
*/
|
|
179
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
180
|
+
// Check for separator keys (e.g., Enter)
|
|
181
|
+
if (separatorKeys.includes(event.key)) {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
if (inputValue.trim()) {
|
|
184
|
+
addTag(inputValue);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle Backspace to remove last tag when input is empty
|
|
190
|
+
if (event.key === 'Backspace' && inputValue === '' && value.length > 0) {
|
|
191
|
+
removeTag(value.length - 1);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Handle Tab to add tag if there's input
|
|
196
|
+
if (event.key === 'Tab' && inputValue.trim()) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
addTag(inputValue);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onkeydown?.(event);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handles focus events
|
|
207
|
+
*/
|
|
208
|
+
const handleFocus = (event: FocusEvent) => {
|
|
209
|
+
isFocused = true;
|
|
210
|
+
onfocus?.(event);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handles blur events
|
|
215
|
+
*/
|
|
216
|
+
const handleBlur = (event: FocusEvent) => {
|
|
217
|
+
isFocused = false;
|
|
218
|
+
|
|
219
|
+
// Add any remaining input as a tag on blur
|
|
220
|
+
if (inputValue.trim()) {
|
|
221
|
+
addTag(inputValue);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
onblur?.(event);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handles wrapper click to focus input
|
|
229
|
+
*/
|
|
230
|
+
const handleWrapperClick = () => {
|
|
231
|
+
focusInput();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Gets the tooltip background color based on alert type
|
|
236
|
+
*/
|
|
237
|
+
const getTooltipColor = (alertType: string) => {
|
|
238
|
+
switch (alertType) {
|
|
239
|
+
case 'error':
|
|
240
|
+
return 'var(--redBackground, #cf313b)';
|
|
241
|
+
case 'warning':
|
|
242
|
+
return 'var(--orangeBackground, #bf4704)';
|
|
243
|
+
case 'success':
|
|
244
|
+
return 'var(--greenBackground, #007a41)';
|
|
245
|
+
case 'info':
|
|
246
|
+
default:
|
|
247
|
+
return 'var(--actionPrimaryBackground, #006acc)';
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
{#snippet tagsInputContent()}
|
|
253
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
254
|
+
<div
|
|
255
|
+
class={wrapperClasses}
|
|
256
|
+
style="width: {width}; min-height: {height};"
|
|
257
|
+
role="group"
|
|
258
|
+
aria-labelledby="{id}-label"
|
|
259
|
+
onclick={handleWrapperClick}
|
|
260
|
+
onkeydown={(e) => e.key === 'Enter' && handleWrapperClick()}
|
|
261
|
+
>
|
|
262
|
+
<div class="tags-input-content">
|
|
263
|
+
{#each value as tag, index (tag)}
|
|
264
|
+
<span class="tag" role="listitem">
|
|
265
|
+
<span class="tag-text">{tag}</span>
|
|
266
|
+
{#if !readonly && !isDisabled}
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class="tag-remove"
|
|
270
|
+
onclick={(e) => {
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
removeTag(index);
|
|
273
|
+
}}
|
|
274
|
+
aria-label="Remove tag {tag}"
|
|
275
|
+
tabindex={-1}
|
|
276
|
+
>
|
|
277
|
+
<TimesIcon />
|
|
278
|
+
</button>
|
|
279
|
+
{/if}
|
|
280
|
+
</span>
|
|
281
|
+
{/each}
|
|
282
|
+
|
|
283
|
+
{#if !readonly}
|
|
284
|
+
<input
|
|
285
|
+
bind:this={inputElement}
|
|
286
|
+
{id}
|
|
287
|
+
type="text"
|
|
288
|
+
class="tags-input-field"
|
|
289
|
+
placeholder={showPlaceholder ? placeholder : ''}
|
|
290
|
+
value={inputValue}
|
|
291
|
+
disabled={isDisabled || !canAddMore}
|
|
292
|
+
oninput={handleInput}
|
|
293
|
+
onkeydown={handleKeydown}
|
|
294
|
+
onfocus={handleFocus}
|
|
295
|
+
onblur={handleBlur}
|
|
296
|
+
aria-label="Add new tag"
|
|
297
|
+
{...restProps}
|
|
298
|
+
/>
|
|
299
|
+
{/if}
|
|
300
|
+
|
|
301
|
+
{#if loading}
|
|
302
|
+
<div class="tags-input-loader">
|
|
303
|
+
<Loader size={14} color="var(--text2)" />
|
|
304
|
+
</div>
|
|
305
|
+
{/if}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{#if children}
|
|
309
|
+
{@render children()}
|
|
310
|
+
{/if}
|
|
311
|
+
</div>
|
|
312
|
+
{/snippet}
|
|
313
|
+
|
|
314
|
+
<Tooltip
|
|
315
|
+
message={hasAlert ? alert?.message || '' : ''}
|
|
316
|
+
placement="top"
|
|
317
|
+
listener="hover"
|
|
318
|
+
listenerout="hover"
|
|
319
|
+
showArrow={true}
|
|
320
|
+
hidden={!hasAlert}
|
|
321
|
+
disabled={!hasAlert || !alert?.message}
|
|
322
|
+
fontColor="var(--actionPrimaryText)"
|
|
323
|
+
width="max-content"
|
|
324
|
+
padding="6px"
|
|
325
|
+
bgColor={getTooltipColor(alert?.type || 'info')}
|
|
326
|
+
class="tags-input-tooltip"
|
|
327
|
+
>
|
|
328
|
+
{#snippet target()}
|
|
329
|
+
{@render tagsInputContent()}
|
|
330
|
+
{/snippet}
|
|
331
|
+
</Tooltip>
|
|
332
|
+
|
|
333
|
+
<style>
|
|
334
|
+
:global(.tags-input-tooltip) {
|
|
335
|
+
padding: 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.tags-input-wrapper {
|
|
339
|
+
position: relative;
|
|
340
|
+
border: 1px solid var(--border3, rgba(255, 255, 255, 0.19));
|
|
341
|
+
border-radius: var(--border-radius, 4px);
|
|
342
|
+
padding: 4px;
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
align-items: flex-start;
|
|
346
|
+
align-content: flex-start;
|
|
347
|
+
background: var(--background1, #1e1e1e);
|
|
348
|
+
min-height: 32px;
|
|
349
|
+
box-shadow:
|
|
350
|
+
0px 16px 16px -16px rgba(0, 0, 0, 0.13) inset,
|
|
351
|
+
0px 12px 12px -12px rgba(0, 0, 0, 0.13) inset,
|
|
352
|
+
0px 8px 8px -8px rgba(0, 0, 0, 0.17) inset,
|
|
353
|
+
0px 4px 4px -4px rgba(0, 0, 0, 0.17) inset,
|
|
354
|
+
0px 3px 3px -3px rgba(0, 0, 0, 0.17) inset,
|
|
355
|
+
0px 1px 1px -1px rgba(0, 0, 0, 0.13) inset;
|
|
356
|
+
cursor: text;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.tags-input-wrapper.focused {
|
|
360
|
+
border-color: var(--blueBorder, #007df0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.tags-input-wrapper.invalid {
|
|
364
|
+
border-color: var(--redBorder, #e42f3a);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.tags-input-wrapper.disabled {
|
|
368
|
+
cursor: not-allowed;
|
|
369
|
+
opacity: 0.5;
|
|
370
|
+
border-color: var(--border1, rgba(255, 255, 255, 0.1));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.tags-input-wrapper.loading {
|
|
374
|
+
cursor: wait;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.tags-input-content {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-wrap: wrap;
|
|
380
|
+
align-items: flex-start;
|
|
381
|
+
align-content: flex-start;
|
|
382
|
+
gap: 4px;
|
|
383
|
+
width: 100%;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.tag {
|
|
387
|
+
position: relative;
|
|
388
|
+
display: flex;
|
|
389
|
+
padding: 4px 8px;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
align-items: center;
|
|
392
|
+
border-radius: var(--border-radius, 4px);
|
|
393
|
+
background: var(--actionSecondaryBackground);
|
|
394
|
+
color: var(--text1, #ebebeb);
|
|
395
|
+
font-size: var(--font-size-small, 11.5px);
|
|
396
|
+
font-weight: var(--font-weight-normal, 400);
|
|
397
|
+
line-height: 16px;
|
|
398
|
+
letter-spacing: -0.115px;
|
|
399
|
+
box-shadow:
|
|
400
|
+
0 0.5px 1px 0 #000,
|
|
401
|
+
0 0.5px 0.5px 0 rgba(255, 255, 255, 0.12) inset;
|
|
402
|
+
user-select: none;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.tag-text {
|
|
406
|
+
max-width: 150px;
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
text-overflow: ellipsis;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.tag-remove {
|
|
413
|
+
position: absolute;
|
|
414
|
+
right: 0;
|
|
415
|
+
top: 0;
|
|
416
|
+
bottom: 0;
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
width: max-content;
|
|
421
|
+
padding: 4px;
|
|
422
|
+
border: none;
|
|
423
|
+
background: #464646;
|
|
424
|
+
color: var(--text2);
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
border-radius: 0 var(--border-radius, 4px) var(--border-radius, 4px) 0;
|
|
427
|
+
opacity: 0;
|
|
428
|
+
pointer-events: none;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.tags-input-wrapper:not(.disabled) .tag:hover .tag-remove {
|
|
432
|
+
opacity: 1;
|
|
433
|
+
pointer-events: auto;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.tag-remove:not(:disabled) {
|
|
437
|
+
color: var(--text1, #ebebeb);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.tag-remove:disabled {
|
|
441
|
+
cursor: not-allowed;
|
|
442
|
+
opacity: 0.5;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.tag-remove :global(svg) {
|
|
446
|
+
width: 10px;
|
|
447
|
+
height: 10px;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.tags-input-field {
|
|
451
|
+
flex: 1;
|
|
452
|
+
min-width: 60px;
|
|
453
|
+
padding: 4px 8px;
|
|
454
|
+
border: none;
|
|
455
|
+
background: transparent;
|
|
456
|
+
color: var(--text1, #ebebeb);
|
|
457
|
+
font-size: var(--font-size-small, 11.5px);
|
|
458
|
+
font-family: inherit;
|
|
459
|
+
line-height: 16px;
|
|
460
|
+
letter-spacing: -0.115px;
|
|
461
|
+
outline: none;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.tags-input-field::placeholder {
|
|
465
|
+
color: var(--text3, #a3a3a3);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.tags-input-field:disabled {
|
|
469
|
+
cursor: not-allowed;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.tags-input-loader {
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
padding: 0 4px;
|
|
477
|
+
}
|
|
478
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { AlertConfig } from '../input/types.js';
|
|
3
|
+
export interface TagsInputProps {
|
|
4
|
+
/**
|
|
5
|
+
* Array of tag values
|
|
6
|
+
*/
|
|
7
|
+
value?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder text when no tags and input is empty
|
|
10
|
+
*/
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Input field id
|
|
14
|
+
*/
|
|
15
|
+
id?: string;
|
|
16
|
+
/**
|
|
17
|
+
* If true, the input field will be disabled
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* If true, the component will show loading state
|
|
22
|
+
*/
|
|
23
|
+
loading?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* If true, the input field will be invalid
|
|
26
|
+
*/
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* If true, the input field will be readonly (no adding/removing tags)
|
|
30
|
+
*/
|
|
31
|
+
readonly?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Defines the alert message to show
|
|
34
|
+
*/
|
|
35
|
+
alert?: AlertConfig | null;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of tags allowed
|
|
38
|
+
*/
|
|
39
|
+
maxTags?: number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Minimum number of tags required (for validation display)
|
|
42
|
+
*/
|
|
43
|
+
minTags?: number | null;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum length of each tag
|
|
46
|
+
*/
|
|
47
|
+
maxTagLength?: number | null;
|
|
48
|
+
/**
|
|
49
|
+
* Separator keys to trigger tag creation (default: ['Enter', ','])
|
|
50
|
+
*/
|
|
51
|
+
separatorKeys?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether to allow duplicate tags (default: false)
|
|
54
|
+
*/
|
|
55
|
+
allowDuplicates?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Custom validation function for tags
|
|
58
|
+
* Return true if valid, false or error message string if invalid
|
|
59
|
+
*/
|
|
60
|
+
validateTag?: (tag: string) => boolean | string;
|
|
61
|
+
/**
|
|
62
|
+
* Whether to trim whitespace from tags (default: true)
|
|
63
|
+
*/
|
|
64
|
+
trimTags?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Custom width for the component
|
|
67
|
+
*/
|
|
68
|
+
width?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Custom height for the component
|
|
71
|
+
*/
|
|
72
|
+
height?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Additional CSS classes
|
|
75
|
+
*/
|
|
76
|
+
class?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Event handler for value changes
|
|
79
|
+
*/
|
|
80
|
+
onValueChange?: (tags: string[]) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Event handler for individual tag addition
|
|
83
|
+
*/
|
|
84
|
+
onTagAdd?: (tag: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Event handler for individual tag removal
|
|
87
|
+
*/
|
|
88
|
+
onTagRemove?: (tag: string, index: number) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Event handler for invalid tag attempt
|
|
91
|
+
*/
|
|
92
|
+
onInvalidTag?: (tag: string, reason: string) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Event handler for focus events
|
|
95
|
+
*/
|
|
96
|
+
onfocus?: (event: FocusEvent) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Event handler for blur events
|
|
99
|
+
*/
|
|
100
|
+
onblur?: (event: FocusEvent) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Event handler for keydown events
|
|
103
|
+
*/
|
|
104
|
+
onkeydown?: (event: KeyboardEvent) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Children content (if any)
|
|
107
|
+
*/
|
|
108
|
+
children?: Snippet;
|
|
109
|
+
}
|
|
110
|
+
export interface TagAddEvent {
|
|
111
|
+
tag: string;
|
|
112
|
+
}
|
|
113
|
+
export interface TagRemoveEvent {
|
|
114
|
+
tag: string;
|
|
115
|
+
index: number;
|
|
116
|
+
}
|
|
117
|
+
export interface InvalidTagEvent {
|
|
118
|
+
tag: string;
|
|
119
|
+
reason: string;
|
|
120
|
+
}
|
|
121
|
+
export interface TagsValueChangeEvent {
|
|
122
|
+
tags: string[];
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|