@celar-ui/svelte 1.3.0 → 1.4.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/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export { default as RadioGroup } from './inputs/RadioGroup.svelte';
29
29
  export { default as RadioItem } from './inputs/RadioItem.svelte';
30
30
  export { default as Switch } from './inputs/Switch.svelte';
31
31
  export { default as Slider } from './inputs/Slider.svelte';
32
+ export { default as TagInput } from './inputs/TagInput.svelte';
32
33
  export { default as AppBar } from './navigation/AppBar.svelte';
33
34
  export { default as NavigationBar } from './navigation/NavigationBar.svelte';
34
35
  export { default as NavigationBarButton } from './navigation/NavigationBarButton.svelte';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export { default as RadioGroup } from './inputs/RadioGroup.svelte';
30
30
  export { default as RadioItem } from './inputs/RadioItem.svelte';
31
31
  export { default as Switch } from './inputs/Switch.svelte';
32
32
  export { default as Slider } from './inputs/Slider.svelte';
33
+ export { default as TagInput } from './inputs/TagInput.svelte';
33
34
  export { default as AppBar } from './navigation/AppBar.svelte';
34
35
  export { default as NavigationBar } from './navigation/NavigationBar.svelte';
35
36
  export { default as NavigationBarButton } from './navigation/NavigationBarButton.svelte';
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import Badge from '../misc/Badge.svelte';
3
+ import type { HTMLInputAttributes } from 'svelte/elements';
4
+
5
+ interface TagInputProps extends Omit<HTMLInputAttributes, 'value'> {
6
+ tags?: string[];
7
+ placeholder?: string;
8
+ maxTags?: number;
9
+ allowDuplicates?: boolean;
10
+ onTagAdd?: (tag: string) => void;
11
+ onTagRemove?: (tag: string, index: number) => void;
12
+ }
13
+
14
+ let {
15
+ tags = $bindable([]),
16
+ placeholder = 'Add tags...',
17
+ maxTags,
18
+ allowDuplicates = false,
19
+ onTagAdd,
20
+ onTagRemove,
21
+ ...rest
22
+ }: TagInputProps = $props();
23
+
24
+ let inputValue = $state('');
25
+
26
+ let inputElement: HTMLInputElement;
27
+
28
+ const addTag = (tag: string) => {
29
+ const trimmedTag = tag.trim();
30
+ if (!trimmedTag) return;
31
+
32
+ if (maxTags !== undefined && tags.length >= maxTags) return;
33
+ if (!allowDuplicates && tags.includes(trimmedTag)) return;
34
+
35
+ tags.push(trimmedTag);
36
+ onTagAdd?.(trimmedTag);
37
+ inputValue = '';
38
+ };
39
+
40
+ const removeTag = (index: number) => {
41
+ const removedTag = tags[index];
42
+ tags.splice(index, 1);
43
+ onTagRemove?.(removedTag, index);
44
+ };
45
+
46
+ const handleKeydown = (event: KeyboardEvent) => {
47
+ if (event.key === 'Enter' || event.key === ',') {
48
+ event.preventDefault();
49
+ addTag(inputValue);
50
+ } else if (event.key === 'Backspace' && !inputValue && tags.length > 0) {
51
+ removeTag(tags.length - 1);
52
+ }
53
+ };
54
+
55
+ const handleInputBlur = () => {
56
+ if (inputValue) {
57
+ addTag(inputValue);
58
+ }
59
+ };
60
+
61
+ const focusInputElement = () => {
62
+ inputElement?.focus();
63
+ };
64
+ </script>
65
+
66
+ <div
67
+ data-tag-input
68
+ onclick={focusInputElement}
69
+ onkeyup={focusInputElement}
70
+ role="button"
71
+ tabindex="0"
72
+ >
73
+ {#each tags as tag, index (index)}
74
+ <Badge size="large">
75
+ {tag}
76
+
77
+ <button
78
+ type="button"
79
+ data-tag-remove
80
+ onclick={() => removeTag(index)}
81
+ aria-label="Remove {tag}"
82
+ >
83
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
84
+ <path
85
+ fill="none"
86
+ stroke="currentColor"
87
+ stroke-linecap="round"
88
+ stroke-linejoin="round"
89
+ stroke-width="2"
90
+ d="M18 6L6 18M6 6l12 12"
91
+ />
92
+ </svg>
93
+ </button>
94
+ </Badge>
95
+ {/each}
96
+
97
+ <input
98
+ {...rest}
99
+ bind:this={inputElement}
100
+ bind:value={inputValue}
101
+ {placeholder}
102
+ onkeydown={handleKeydown}
103
+ onblur={handleInputBlur}
104
+ disabled={maxTags !== undefined && tags.length >= maxTags}
105
+ />
106
+ </div>
107
+
108
+ <style>[data-tag-input] {
109
+ --color-background: transparent;
110
+ display: flex;
111
+ align-items: center;
112
+ flex-wrap: wrap;
113
+ gap: var(--gap--sm);
114
+ position: relative;
115
+ width: 100%;
116
+ box-sizing: border-box;
117
+ transition-duration: var(--transition-dur);
118
+ transition-property: border-color;
119
+ transition-timing-function: ease-in-out;
120
+ border: 1px solid var(--color-border);
121
+ border-radius: var(--radius);
122
+ background-color: var(--color-background);
123
+ padding: var(--gap--md);
124
+ cursor: text;
125
+ }
126
+ [data-tag-input]:focus-within {
127
+ border: 1px solid var(--color-primary);
128
+ }
129
+ [data-tag-input] input {
130
+ flex: 1;
131
+ border: none;
132
+ outline: none;
133
+ background: transparent;
134
+ font-size: inherit;
135
+ font-family: inherit;
136
+ color: inherit;
137
+ }
138
+ [data-tag-input] input::-moz-placeholder {
139
+ opacity: 1;
140
+ color: rgba(var(--color-onSurface--rgb), 0.7);
141
+ }
142
+ [data-tag-input] input::placeholder {
143
+ opacity: 1;
144
+ color: rgba(var(--color-onSurface--rgb), 0.7);
145
+ }
146
+ [data-tag-input] input:disabled {
147
+ opacity: 0.5;
148
+ cursor: not-allowed;
149
+ }
150
+ [data-tag-input] [data-tag-remove] {
151
+ display: inline-flex;
152
+ justify-content: center;
153
+ align-items: center;
154
+ background-color: transparent;
155
+ border: none;
156
+ color: inherit;
157
+ cursor: pointer;
158
+ padding: var(--gap--xs);
159
+ border-radius: 50%;
160
+ aspect-ratio: 1;
161
+ transition-duration: var(--transition-dur);
162
+ transition-property: background-color;
163
+ transition-timing-function: ease-in-out;
164
+ margin-left: var(--gap--sm);
165
+ }
166
+ [data-tag-input] [data-tag-remove]:hover {
167
+ background-color: var(--color-surfaceContainer);
168
+ }
169
+ [data-tag-input] [data-tag-remove]:focus {
170
+ outline: 1px solid var(--color-border);
171
+ }
172
+ [data-tag-input] [data-tag-remove] svg {
173
+ width: var(--gap);
174
+ }</style>
@@ -0,0 +1,12 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ interface TagInputProps extends Omit<HTMLInputAttributes, 'value'> {
3
+ tags?: string[];
4
+ placeholder?: string;
5
+ maxTags?: number;
6
+ allowDuplicates?: boolean;
7
+ onTagAdd?: (tag: string) => void;
8
+ onTagRemove?: (tag: string, index: number) => void;
9
+ }
10
+ declare const TagInput: import("svelte").Component<TagInputProps, {}, "tags">;
11
+ type TagInput = ReturnType<typeof TagInput>;
12
+ export default TagInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celar-ui/svelte",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "cuikho210",