@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.
@@ -17,6 +17,7 @@ export * from './section';
17
17
  export * from './select';
18
18
  export * from './shared';
19
19
  export * from './switch';
20
+ export * from './tags';
20
21
  export * from './text';
21
22
  export * from './tooltip';
22
23
  export { default as Loader } from './Loader.svelte';
@@ -17,6 +17,7 @@ export * from './section';
17
17
  export * from './select';
18
18
  export * from './shared';
19
19
  export * from './switch';
20
+ export * from './tags';
20
21
  export * from './text';
21
22
  export * from './tooltip';
22
23
  export { default as Loader } from './Loader.svelte';
@@ -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,4 @@
1
+ import type { TagsInputProps } from './types.js';
2
+ declare const TagsInput: import("svelte").Component<TagsInputProps, {}, "value">;
3
+ type TagsInput = ReturnType<typeof TagsInput>;
4
+ export default TagsInput;
@@ -0,0 +1,2 @@
1
+ export { default as TagsInput } from './TagsInput.svelte';
2
+ export * from './types.js';
@@ -0,0 +1,2 @@
1
+ export { default as TagsInput } from './TagsInput.svelte';
2
+ export * from './types.js';
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.50",
3
+ "version": "1.0.51",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {