mertani-web-toolkit 0.1.48 → 0.1.50

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.
@@ -0,0 +1,326 @@
1
+ .input-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ width: 100%;
5
+ }
6
+
7
+ .input-container.input-side {
8
+ flex-direction: row;
9
+ align-items: center;
10
+ gap: 12px;
11
+ }
12
+
13
+ .input-label {
14
+ font-size: 14px;
15
+ font-weight: 400;
16
+ color: var(--color-text-primary);
17
+ margin-bottom: 4px;
18
+ display: block;
19
+ }
20
+
21
+ .input-container.input-side .input-label {
22
+ margin-bottom: 0;
23
+ min-width: 120px;
24
+ }
25
+
26
+ .input-label.label-left {
27
+ text-align: left;
28
+ }
29
+
30
+ .input-label.label-right {
31
+ text-align: right;
32
+ }
33
+
34
+ .label-subLabel {
35
+ font-weight: 400;
36
+ color: var(--color-text-tertiary);
37
+ margin-left: 4px;
38
+ }
39
+
40
+ .label-required {
41
+ color: var(--color-text-error-ti);
42
+ margin-left: 4px;
43
+ }
44
+
45
+ .input-wrapper {
46
+ position: relative;
47
+ display: flex;
48
+ align-items: stretch;
49
+ width: 100%;
50
+ border: 1px solid var(--color-border-form);
51
+ border-radius: 6px;
52
+ transition:
53
+ border-color 0.2s,
54
+ box-shadow 0.2s;
55
+ }
56
+
57
+ .input-wrapper:has(input:disabled) {
58
+ background: var(--color-bg-disabled);
59
+ border-color: var(--color-border-disabled);
60
+ }
61
+
62
+ .input-wrapper.input-loading {
63
+ cursor: wait;
64
+ }
65
+
66
+ .input-wrapper.input-disabled {
67
+ opacity: 0.6;
68
+ cursor: not-allowed;
69
+ }
70
+
71
+ .input-wrapper.input-error {
72
+ border-color: var(--color-text-error-ti);
73
+ }
74
+
75
+ .search-by-wrapper {
76
+ position: relative;
77
+ display: flex;
78
+ align-items: center;
79
+ border-right: 1px solid var(--color-border-form);
80
+ background: var(--color-bg-disabled);
81
+ flex-shrink: 0;
82
+ border-radius: 6px 0 0 6px;
83
+ }
84
+
85
+ .search-by-trigger {
86
+ border: none;
87
+ outline: none;
88
+ background: transparent;
89
+ font-size: 12px;
90
+ font-weight: 600;
91
+ color: var(--color-text-primary);
92
+ cursor: pointer;
93
+ padding: 0 10px;
94
+ height: 100%;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ }
99
+
100
+ .search-by-trigger:disabled {
101
+ cursor: not-allowed;
102
+ opacity: 0.7;
103
+ }
104
+
105
+ .search-by-label {
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .search-by-menu {
110
+ position: absolute;
111
+ top: calc(100% + 4px);
112
+ min-width: 140px;
113
+ background: var(--color-bg-surface);
114
+ border: 1px solid var(--color-border-form);
115
+ border-radius: 4px;
116
+ box-shadow:
117
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
118
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
119
+ overflow: hidden;
120
+ z-index: 99999;
121
+ }
122
+
123
+ .search-by-item {
124
+ width: 100%;
125
+ padding: 10px 12px;
126
+ text-align: left;
127
+ background: transparent;
128
+ border: none;
129
+ font-size: 13px;
130
+ color: var(--color-text-primary);
131
+ cursor: pointer;
132
+ transition:
133
+ background-color 0.15s,
134
+ color 0.15s;
135
+ }
136
+
137
+ .search-by-item + .search-by-item {
138
+ border-top: 1px solid color-mix(in srgb, var(--color-border-form) 45%, transparent);
139
+ }
140
+
141
+ .search-by-item:hover,
142
+ .search-by-item.active {
143
+ background: var(--color-bg-disabled);
144
+ }
145
+
146
+ .prefix-wrapper {
147
+ display: flex;
148
+ align-items: center;
149
+ padding: 0.625rem 0.75rem;
150
+ background: var(--color-bg-disabled);
151
+ border-right: 1px solid var(--color-border-form);
152
+ flex-shrink: 0;
153
+ }
154
+
155
+ .prefix-text {
156
+ font-size: 0.75rem;
157
+ font-weight: 500;
158
+ color: var(--color-text-primary);
159
+ white-space: nowrap;
160
+ }
161
+
162
+ .suffix-wrapper {
163
+ display: flex;
164
+ align-items: center;
165
+ padding: 0.625rem 0.75rem;
166
+ background: var(--color-bg-disabled);
167
+ border-left: 1px solid var(--color-border-form);
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ .suffix-text {
172
+ font-size: 0.75rem;
173
+ font-weight: 500;
174
+ color: var(--color-text-primary);
175
+ white-space: nowrap;
176
+ }
177
+
178
+ .input-field {
179
+ flex: 1;
180
+ border: none;
181
+ outline: none;
182
+ background: transparent;
183
+ padding: 0.625rem 1rem;
184
+ font-size: 14px;
185
+ color: var(--color-text-primary);
186
+ width: 100%;
187
+ }
188
+
189
+ .input-wrapper:has(.prefix-wrapper) .input-field {
190
+ padding-left: 0.75rem;
191
+ }
192
+
193
+ .input-wrapper:has(.suffix-wrapper) .input-field {
194
+ padding-right: 0.75rem;
195
+ }
196
+
197
+ .input-wrapper:has(.input-icon-right):not(:has(.suffix-wrapper)) .input-field {
198
+ padding-right: 2.5rem;
199
+ }
200
+
201
+ .input-wrapper:has(.input-loader.input-icon-right):not(:has(.suffix-wrapper)) .input-field {
202
+ padding-right: 2.5rem;
203
+ }
204
+
205
+ .input-wrapper:has(.input-icon-left):not(:has(.prefix-wrapper)) .input-field {
206
+ padding-left: 2.5rem;
207
+ }
208
+
209
+ .input-wrapper:has(.input-loader.input-icon-left):not(:has(.prefix-wrapper)) .input-field {
210
+ padding-left: 2.5rem;
211
+ }
212
+
213
+ .input-field:disabled {
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ .input-field::placeholder {
218
+ color: var(--color-text-tertiary);
219
+ }
220
+
221
+ .input-icon {
222
+ position: absolute;
223
+ top: 50%;
224
+ transform: translateY(-50%);
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ pointer-events: none;
229
+ }
230
+
231
+ .input-icon-left {
232
+ left: 12px;
233
+ }
234
+
235
+ .input-icon-right {
236
+ right: 12px;
237
+ }
238
+
239
+ .input-loader {
240
+ position: absolute;
241
+ top: 50%;
242
+ transform: translateY(-50%);
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ pointer-events: none;
247
+ }
248
+
249
+ .input-loader.input-icon-left {
250
+ left: 12px;
251
+ }
252
+
253
+ .input-loader.input-icon-right {
254
+ right: 12px;
255
+ }
256
+
257
+ .loader-spinner {
258
+ display: inline-block;
259
+ width: 16px;
260
+ height: 16px;
261
+ border: 2px solid transparent;
262
+ border-top-color: currentColor;
263
+ border-right-color: currentColor;
264
+ border-radius: 50%;
265
+ animation: spinner-rotate 0.8s linear infinite;
266
+ }
267
+
268
+ @keyframes spinner-rotate {
269
+ 0% {
270
+ transform: rotate(0deg);
271
+ }
272
+ 100% {
273
+ transform: rotate(360deg);
274
+ }
275
+ }
276
+
277
+ .error-message {
278
+ margin-top: 4px;
279
+ font-size: 12px;
280
+ color: var(--color-text-error-ti);
281
+ }
282
+
283
+ .suggestions-menu {
284
+ position: absolute;
285
+ left: 0;
286
+ right: 0;
287
+ top: calc(100% + 4px);
288
+ z-index: 20;
289
+ background: var(--color-bg-surface);
290
+ border: 1px solid var(--color-border-form);
291
+ border-radius: 6px;
292
+ box-shadow:
293
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
294
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
295
+ overflow: hidden;
296
+ max-height: 240px;
297
+ overflow-y: auto;
298
+ }
299
+
300
+ .suggestion-item {
301
+ width: 100%;
302
+ padding: 8px 12px;
303
+ text-align: left;
304
+ background: transparent;
305
+ border: none;
306
+ font-size: 14px;
307
+ color: var(--color-text-primary);
308
+ cursor: pointer;
309
+ }
310
+
311
+ .suggestion-item:hover,
312
+ .suggestion-item.active {
313
+ background: var(--color-bg-disabled);
314
+ }
315
+
316
+ .suggestion-empty {
317
+ padding: 8px 12px;
318
+ font-size: 13px;
319
+ color: var(--color-text-tertiary);
320
+ }
321
+
322
+ .suggestion-loading {
323
+ padding: 8px 12px;
324
+ font-size: 13px;
325
+ color: var(--color-text-tertiary);
326
+ }
@@ -0,0 +1,612 @@
1
+ <script lang="ts">
2
+ import { Icon } from '../../../index.js';
3
+ import type { TIconName } from '../../../icons/index.js';
4
+ import './TextInputSuggestion.css';
5
+ import type { HTMLInputAttributes } from 'svelte/elements';
6
+
7
+ type SuggestionOption =
8
+ | string
9
+ | {
10
+ label: string;
11
+ id?: string | number;
12
+ value?: string;
13
+ detail?: unknown;
14
+ };
15
+ type SearchByOption = {
16
+ label: string;
17
+ value: string;
18
+ };
19
+
20
+ interface Props extends HTMLInputAttributes {
21
+ // ===Styles===
22
+ // Label
23
+ labelColor?: string;
24
+ aligment?: 'side' | 'top';
25
+ position?: 'left' | 'right';
26
+
27
+ // Field
28
+ size?: 48 | 40 | 32;
29
+ backgroundColor?: string;
30
+ borderColor?: string;
31
+ accentColor?: string;
32
+ textColor?: string;
33
+ errorColor?: string;
34
+ icon?: TIconName;
35
+ iconColor?: string;
36
+ iconPosition?: 'left' | 'right';
37
+ borderRadius?: number;
38
+ boxShadow?: string;
39
+
40
+ // ===Properties===
41
+ // Data
42
+ id?: string;
43
+ label?: string;
44
+ subLabel?: string;
45
+ placeholder?: string;
46
+ value?: string;
47
+ prefix?: string;
48
+ suffix?: string;
49
+
50
+ // Suggestions
51
+ suggestions?: SuggestionOption[];
52
+ searchByOptions?: SearchByOption[];
53
+ searchBy?: string;
54
+ minChars?: number;
55
+ maxSuggestions?: number;
56
+ showSuggestionsOnFocus?: boolean;
57
+ emptyMessage?: string;
58
+ closeOnSelect?: boolean;
59
+ loadingSuggestions?: boolean;
60
+
61
+ // Events
62
+ onSelectSuggestion?: (value: string, option: SuggestionOption) => void;
63
+ onclick?: (event: MouseEvent) => void;
64
+ oninput?: (event: Event) => void;
65
+ onchange?: (event: Event) => void;
66
+ onfocus?: (event: FocusEvent) => void;
67
+ onblur?: (event: FocusEvent) => void;
68
+ onkeydown?: (event: KeyboardEvent) => void;
69
+
70
+ // Validation
71
+ isMandatory?: boolean;
72
+ regex?: RegExp;
73
+ minLength?: number;
74
+ maxLength?: number;
75
+ customValidation?: (value: string) => string | null;
76
+
77
+ // Additional Actions
78
+ isLoading?: boolean;
79
+ isShow?: boolean;
80
+ disabled?: boolean;
81
+ readOnly?: boolean;
82
+ tooltip?: string;
83
+
84
+ // Any
85
+ class?: string;
86
+ style?: string;
87
+ }
88
+
89
+ let {
90
+ // ===Styles===
91
+ // Label
92
+ labelColor = 'var(--color-text-primary)',
93
+ aligment = 'top',
94
+ position = 'left',
95
+
96
+ // Field
97
+ size = 40,
98
+ backgroundColor = 'var(--color-bg-surface)',
99
+ borderColor = 'var(--color-border-form)',
100
+ accentColor = 'var(--color-bg-act-primary)',
101
+ textColor = 'var(--color-text-primary)',
102
+ errorColor = 'var(--color-text-error-ti)',
103
+ icon,
104
+ iconColor = 'var(--color-text-primary)',
105
+ iconPosition = 'right',
106
+ borderRadius,
107
+ boxShadow = '',
108
+
109
+ // ===Properties===
110
+ // Data
111
+ id = '',
112
+ label = '',
113
+ subLabel = '',
114
+ placeholder = '',
115
+ value = $bindable(''),
116
+ prefix = '',
117
+ suffix = '',
118
+
119
+ // Suggestions
120
+ suggestions = [],
121
+ searchByOptions = [],
122
+ searchBy = $bindable(''),
123
+ minChars = 1,
124
+ maxSuggestions = 8,
125
+ showSuggestionsOnFocus = true,
126
+ emptyMessage = 'Tidak ada hasil',
127
+ closeOnSelect = true,
128
+ loadingSuggestions = false,
129
+
130
+ // Events
131
+ onSelectSuggestion,
132
+ onclick,
133
+ oninput,
134
+ onchange,
135
+ onfocus,
136
+ onblur,
137
+ onkeydown,
138
+
139
+ // Validation
140
+ isMandatory = false,
141
+ regex,
142
+ minLength,
143
+ maxLength,
144
+ customValidation,
145
+
146
+ // Additional Actions
147
+ isLoading = false,
148
+ isShow = true,
149
+ disabled = false,
150
+ readOnly = false,
151
+ tooltip = '',
152
+
153
+ class: className = '',
154
+ style: customStyle = '',
155
+ ...props
156
+ }: Props = $props();
157
+
158
+ let inputValue = $state(value);
159
+ let errorMessage = $state('');
160
+ let isFocused = $state(false);
161
+ let showList = $state(false);
162
+ let activeIndex = $state(-1);
163
+ let wrapperEl: HTMLDivElement | null = $state(null);
164
+ let searchByValue = $state(searchBy);
165
+ let searchByOpen = $state(false);
166
+
167
+ $effect(() => {
168
+ inputValue = value;
169
+ });
170
+
171
+ $effect(() => {
172
+ searchByValue = searchBy;
173
+ });
174
+
175
+ $effect(() => {
176
+ if (!searchByValue && searchByOptions.length) {
177
+ searchByValue = searchByOptions[0].value;
178
+ searchBy = searchByValue;
179
+ }
180
+ });
181
+
182
+ const sizeConfig = $derived(() => {
183
+ switch (size) {
184
+ case 48:
185
+ return {
186
+ height: '48px',
187
+ labelFontSize: '16px',
188
+ inputFontSize: '16px',
189
+ borderRadius: 8,
190
+ padding: '8px 12px'
191
+ };
192
+ case 40:
193
+ return {
194
+ height: '40px',
195
+ labelFontSize: '14px',
196
+ inputFontSize: '16px',
197
+ borderRadius: 8,
198
+ padding: '6px 12px'
199
+ };
200
+ case 32:
201
+ return {
202
+ height: '32px',
203
+ labelFontSize: '14px',
204
+ inputFontSize: '14px',
205
+ borderRadius: 6,
206
+ padding: '6px 12px'
207
+ };
208
+ default:
209
+ return {
210
+ height: '40px',
211
+ labelFontSize: '14px',
212
+ inputFontSize: '16px',
213
+ borderRadius: 8,
214
+ padding: '6px 12px'
215
+ };
216
+ }
217
+ });
218
+
219
+ const wrapperStyles = $derived(() => {
220
+ const styles: string[] = [];
221
+ styles.push(`height: ${sizeConfig().height};`);
222
+
223
+ if (borderColor) {
224
+ styles.push(`border-color: ${borderColor};`);
225
+ }
226
+
227
+ const radius = borderRadius || sizeConfig().borderRadius;
228
+ styles.push(`border-radius: ${radius}px;`);
229
+
230
+ if (boxShadow) {
231
+ styles.push(`box-shadow: ${boxShadow};`);
232
+ }
233
+
234
+ if (isFocused && accentColor) {
235
+ styles.push(`border-color: ${accentColor};`);
236
+ styles.push(`box-shadow: 0 0 0 2px ${accentColor}40;`);
237
+ }
238
+
239
+ if (errorMessage && errorColor) {
240
+ styles.push(`border-color: ${errorColor};`);
241
+ }
242
+
243
+ if (disabled) {
244
+ styles.push(`background: var(--color-bg-disabled);`);
245
+ } else if (backgroundColor) {
246
+ styles.push(`background: ${backgroundColor};`);
247
+ }
248
+
249
+ if (customStyle) {
250
+ styles.push(customStyle);
251
+ }
252
+
253
+ return styles.join(' ');
254
+ });
255
+
256
+ const prefixStyles = $derived(() => {
257
+ const radius = borderRadius || sizeConfig().borderRadius;
258
+ return `border-radius: ${radius}px 0 0 ${radius}px;`;
259
+ });
260
+
261
+ const suffixStyles = $derived(() => {
262
+ const radius = borderRadius || sizeConfig().borderRadius;
263
+ return `border-radius: 0 ${radius}px ${radius}px 0;`;
264
+ });
265
+
266
+ const inputStyles = $derived(() => {
267
+ const styles: string[] = [];
268
+ styles.push(`font-size: ${sizeConfig().inputFontSize};`);
269
+ styles.push(`padding: ${sizeConfig().padding};`);
270
+
271
+ if (textColor) {
272
+ styles.push(`color: ${textColor};`);
273
+ }
274
+
275
+ return styles.join(' ');
276
+ });
277
+
278
+ const labelStyles = $derived(() => {
279
+ const styles: string[] = [];
280
+ styles.push(`font-size: ${sizeConfig().labelFontSize};`);
281
+
282
+ if (labelColor) {
283
+ styles.push(`color: ${labelColor};`);
284
+ }
285
+
286
+ return styles.join(' ');
287
+ });
288
+
289
+ const wrapperClasses = $derived(() => {
290
+ const classes = ['input-wrapper'];
291
+
292
+ if (isLoading) {
293
+ classes.push('input-loading');
294
+ }
295
+
296
+ if (disabled) {
297
+ classes.push('input-disabled');
298
+ }
299
+
300
+ if (errorMessage) {
301
+ classes.push('input-error');
302
+ }
303
+
304
+ if (className) {
305
+ classes.push(className);
306
+ }
307
+
308
+ return classes.join(' ');
309
+ });
310
+
311
+ function normalizeSuggestion(option: SuggestionOption) {
312
+ if (typeof option === 'string') {
313
+ return { label: option, value: option, raw: option };
314
+ }
315
+ const val = option.value ?? option.label;
316
+ return { label: option.label, value: val, raw: option };
317
+ }
318
+
319
+ const filteredSuggestions = $derived(() => {
320
+ const normalized = suggestions.map(normalizeSuggestion);
321
+ const query = inputValue.trim().toLowerCase();
322
+
323
+ if (query.length < minChars && !(showSuggestionsOnFocus && isFocused && minChars === 0)) {
324
+ return [];
325
+ }
326
+
327
+ const result = query
328
+ ? normalized.filter((item) => item.label.toLowerCase().includes(query))
329
+ : normalized;
330
+ return result.slice(0, maxSuggestions);
331
+ });
332
+
333
+ const shouldShowEmpty = $derived(() => {
334
+ const query = inputValue.trim();
335
+ if (!showList) return false;
336
+ if (query.length < minChars && !(showSuggestionsOnFocus && isFocused && minChars === 0))
337
+ return false;
338
+ return !loadingSuggestions && suggestions.length > 0 && filteredSuggestions().length === 0;
339
+ });
340
+
341
+ function openSuggestions() {
342
+ if (disabled || isLoading) return;
343
+ showList = true;
344
+ }
345
+
346
+ function closeSuggestions() {
347
+ showList = false;
348
+ activeIndex = -1;
349
+ }
350
+
351
+ function validateInput(value: string): string {
352
+ if (customValidation) {
353
+ const customError = customValidation(value);
354
+ if (customError) {
355
+ return customError;
356
+ }
357
+ }
358
+
359
+ if (isMandatory && !value.trim()) {
360
+ return 'Field ini wajib diisi';
361
+ }
362
+
363
+ if (minLength !== undefined && value.length < minLength) {
364
+ return `Minimal ${minLength} karakter`;
365
+ }
366
+
367
+ if (maxLength !== undefined && value.length > maxLength) {
368
+ return `Maksimal ${maxLength} karakter`;
369
+ }
370
+
371
+ if (regex && value && !regex.test(value)) {
372
+ return 'Format tidak valid';
373
+ }
374
+
375
+ return '';
376
+ }
377
+
378
+ function handleInput(e: Event) {
379
+ const target = e.target as HTMLInputElement;
380
+ const nextValue = target.value;
381
+ inputValue = nextValue;
382
+ // keep parent in sync
383
+ value = inputValue;
384
+ errorMessage = validateInput(nextValue);
385
+ openSuggestions();
386
+ oninput?.(e);
387
+ }
388
+
389
+ function handleSearchByChange(e: Event) {
390
+ const target = e.target as HTMLSelectElement;
391
+ searchByValue = target.value;
392
+ searchBy = target.value;
393
+ }
394
+
395
+ function selectSearchBy(option: SearchByOption) {
396
+ searchByValue = option.value;
397
+ searchBy = option.value;
398
+ searchByOpen = false;
399
+ }
400
+
401
+ function getSearchByLabel() {
402
+ const current = searchByOptions.find((opt) => opt.value === searchByValue);
403
+ return current?.label || 'Search';
404
+ }
405
+
406
+ function handleChange(e: Event) {
407
+ const target = e.target as HTMLInputElement;
408
+ const value = target.value;
409
+ errorMessage = validateInput(value);
410
+ onchange?.(e);
411
+ }
412
+
413
+ function handleFocus(e: FocusEvent) {
414
+ isFocused = true;
415
+ if (showSuggestionsOnFocus) {
416
+ openSuggestions();
417
+ }
418
+ onfocus?.(e);
419
+ }
420
+
421
+ function handleBlur(e: FocusEvent) {
422
+ isFocused = false;
423
+ const target = e.target as HTMLInputElement;
424
+ errorMessage = validateInput(target.value);
425
+ onblur?.(e);
426
+ }
427
+
428
+ function handleKeyDown(e: KeyboardEvent) {
429
+ const list = filteredSuggestions();
430
+ if (list.length > 0) {
431
+ if (e.key === 'ArrowDown') {
432
+ e.preventDefault();
433
+ openSuggestions();
434
+ activeIndex = Math.min(activeIndex + 1, list.length - 1);
435
+ } else if (e.key === 'ArrowUp') {
436
+ e.preventDefault();
437
+ openSuggestions();
438
+ activeIndex = Math.max(activeIndex - 1, 0);
439
+ } else if (e.key === 'Enter' && showList && activeIndex >= 0) {
440
+ e.preventDefault();
441
+ selectSuggestion(list[activeIndex]);
442
+ } else if (e.key === 'Escape') {
443
+ closeSuggestions();
444
+ }
445
+ }
446
+ onkeydown?.(e);
447
+ }
448
+
449
+ function handleClick(e: MouseEvent) {
450
+ if (!disabled && !isLoading && onclick) {
451
+ onclick(e);
452
+ }
453
+ }
454
+
455
+ function selectSuggestion(item: { label: string; value: string; raw: SuggestionOption }) {
456
+ inputValue = item.value;
457
+ value = item.value;
458
+ onSelectSuggestion?.(item.value, item.raw);
459
+ if (closeOnSelect) {
460
+ closeSuggestions();
461
+ }
462
+ }
463
+
464
+ function handleClickOutside(e: MouseEvent) {
465
+ if (wrapperEl && !wrapperEl.contains(e.target as Node)) {
466
+ closeSuggestions();
467
+ searchByOpen = false;
468
+ }
469
+ }
470
+
471
+ $effect(() => {
472
+ if (showList) {
473
+ setTimeout(() => document.addEventListener('click', handleClickOutside), 10);
474
+ } else {
475
+ document.removeEventListener('click', handleClickOutside);
476
+ }
477
+ return () => document.removeEventListener('click', handleClickOutside);
478
+ });
479
+ </script>
480
+
481
+ {#if isShow}
482
+ <div class="input-container" class:input-side={aligment === 'side'}>
483
+ {#if label}
484
+ <label
485
+ for={id}
486
+ class="input-label"
487
+ class:label-left={position === 'left'}
488
+ class:label-right={position === 'right'}
489
+ style={labelStyles()}
490
+ >
491
+ {label}
492
+ {#if subLabel}
493
+ <span class="label-subLabel">{subLabel}</span>
494
+ {/if}
495
+ {#if isMandatory}
496
+ <span class="label-required">*</span>
497
+ {/if}
498
+ </label>
499
+ {/if}
500
+ <div class={wrapperClasses()} style={wrapperStyles()} bind:this={wrapperEl}>
501
+ {#if searchByOptions.length}
502
+ <div class="search-by-wrapper">
503
+ <button
504
+ type="button"
505
+ class="search-by-trigger"
506
+ onclick={() => !disabled && !isLoading && (searchByOpen = !searchByOpen)}
507
+ disabled={disabled || isLoading}
508
+ >
509
+ <span class="search-by-label">{getSearchByLabel()}</span>
510
+ <Icon
511
+ name="bs-chevron-down"
512
+ style="transform-origin: center; transition: transform 0.25s; transform: rotate({searchByOpen
513
+ ? '180deg'
514
+ : '0deg'});"
515
+ />
516
+ </button>
517
+ {#if searchByOpen}
518
+ <div class="search-by-menu">
519
+ {#each searchByOptions as option (option.value)}
520
+ <button
521
+ type="button"
522
+ class="search-by-item"
523
+ class:active={option.value === searchByValue}
524
+ onmousedown={(e) => {
525
+ e.preventDefault();
526
+ selectSearchBy(option);
527
+ }}
528
+ >
529
+ {option.label}
530
+ </button>
531
+ {/each}
532
+ </div>
533
+ {/if}
534
+ </div>
535
+ {/if}
536
+ {#if prefix}
537
+ <div class="prefix-wrapper" style={prefixStyles()}>
538
+ <span class="prefix-text">{prefix}</span>
539
+ </div>
540
+ {/if}
541
+ {#if isLoading && iconPosition === 'left'}
542
+ <div class="input-loader input-icon-left">
543
+ <span class="loader-spinner"></span>
544
+ </div>
545
+ {:else if icon && iconPosition === 'left' && !prefix}
546
+ <div class="input-icon input-icon-left">
547
+ <Icon name={icon} color={iconColor || 'currentColor'} width={16} height={16} />
548
+ </div>
549
+ {/if}
550
+ <input
551
+ {id}
552
+ class="input-field"
553
+ style={inputStyles()}
554
+ value={inputValue}
555
+ {placeholder}
556
+ disabled={disabled || isLoading}
557
+ readonly={readOnly}
558
+ oninput={handleInput}
559
+ onchange={handleChange}
560
+ onfocus={handleFocus}
561
+ onblur={handleBlur}
562
+ onkeydown={handleKeyDown}
563
+ onclick={handleClick}
564
+ title={tooltip || ''}
565
+ {...props}
566
+ />
567
+ {#if isLoading && iconPosition === 'right'}
568
+ <div class="input-loader input-icon-right">
569
+ <span class="loader-spinner"></span>
570
+ </div>
571
+ {:else if icon && iconPosition === 'right' && !suffix}
572
+ <div class="input-icon input-icon-right">
573
+ <Icon name={icon} color={iconColor || 'currentColor'} width={16} height={16} />
574
+ </div>
575
+ {/if}
576
+ {#if suffix}
577
+ <div class="suffix-wrapper" style={suffixStyles()}>
578
+ <span class="suffix-text">{suffix}</span>
579
+ </div>
580
+ {/if}
581
+
582
+ {#if showList}
583
+ <div class="suggestions-menu">
584
+ {#if loadingSuggestions}
585
+ <div class="suggestion-loading">Loading...</div>
586
+ {:else if filteredSuggestions().length}
587
+ {#each filteredSuggestions() as item, index (item.value)}
588
+ <button
589
+ type="button"
590
+ class="suggestion-item"
591
+ class:active={index === activeIndex}
592
+ onmousedown={(e) => {
593
+ e.preventDefault();
594
+ selectSuggestion(item);
595
+ }}
596
+ >
597
+ {item.label}
598
+ </button>
599
+ {/each}
600
+ {:else if shouldShowEmpty()}
601
+ <div class="suggestion-empty">{emptyMessage}</div>
602
+ {/if}
603
+ </div>
604
+ {/if}
605
+ </div>
606
+ {#if errorMessage}
607
+ <p class="error-message" style={errorColor ? `color: ${errorColor};` : ''}>
608
+ {errorMessage}
609
+ </p>
610
+ {/if}
611
+ </div>
612
+ {/if}
@@ -0,0 +1,67 @@
1
+ import type { TIconName } from '../../../icons/index.js';
2
+ import './TextInputSuggestion.css';
3
+ import type { HTMLInputAttributes } from 'svelte/elements';
4
+ type SuggestionOption = string | {
5
+ label: string;
6
+ id?: string | number;
7
+ value?: string;
8
+ detail?: unknown;
9
+ };
10
+ type SearchByOption = {
11
+ label: string;
12
+ value: string;
13
+ };
14
+ interface Props extends HTMLInputAttributes {
15
+ labelColor?: string;
16
+ aligment?: 'side' | 'top';
17
+ position?: 'left' | 'right';
18
+ size?: 48 | 40 | 32;
19
+ backgroundColor?: string;
20
+ borderColor?: string;
21
+ accentColor?: string;
22
+ textColor?: string;
23
+ errorColor?: string;
24
+ icon?: TIconName;
25
+ iconColor?: string;
26
+ iconPosition?: 'left' | 'right';
27
+ borderRadius?: number;
28
+ boxShadow?: string;
29
+ id?: string;
30
+ label?: string;
31
+ subLabel?: string;
32
+ placeholder?: string;
33
+ value?: string;
34
+ prefix?: string;
35
+ suffix?: string;
36
+ suggestions?: SuggestionOption[];
37
+ searchByOptions?: SearchByOption[];
38
+ searchBy?: string;
39
+ minChars?: number;
40
+ maxSuggestions?: number;
41
+ showSuggestionsOnFocus?: boolean;
42
+ emptyMessage?: string;
43
+ closeOnSelect?: boolean;
44
+ loadingSuggestions?: boolean;
45
+ onSelectSuggestion?: (value: string, option: SuggestionOption) => void;
46
+ onclick?: (event: MouseEvent) => void;
47
+ oninput?: (event: Event) => void;
48
+ onchange?: (event: Event) => void;
49
+ onfocus?: (event: FocusEvent) => void;
50
+ onblur?: (event: FocusEvent) => void;
51
+ onkeydown?: (event: KeyboardEvent) => void;
52
+ isMandatory?: boolean;
53
+ regex?: RegExp;
54
+ minLength?: number;
55
+ maxLength?: number;
56
+ customValidation?: (value: string) => string | null;
57
+ isLoading?: boolean;
58
+ isShow?: boolean;
59
+ disabled?: boolean;
60
+ readOnly?: boolean;
61
+ tooltip?: string;
62
+ class?: string;
63
+ style?: string;
64
+ }
65
+ declare const TextInputSuggestion: import("svelte").Component<Props, {}, "value" | "searchBy">;
66
+ type TextInputSuggestion = ReturnType<typeof TextInputSuggestion>;
67
+ export default TextInputSuggestion;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { default as Button } from './components/Button/Button.svelte';
2
2
  export { default as Icon } from './components/Icon/Icon.svelte';
3
3
  export { default as TextInput } from './components/inputs/TextInput/TextInput.svelte';
4
+ export { default as TextInputSuggestion } from './components/inputs/TextInputSuggestion/TextInputSuggestion.svelte';
4
5
  export { default as TextareaInput } from './components/inputs/TextareaInput/TextareaInput.svelte';
5
6
  export { default as SelectInput } from './components/inputs/SelectInput/SelectInput.svelte';
6
7
  export { default as MultiSelectInput } from './components/inputs/MultiSelectInput/MultiSelectInput.svelte';
8
+ export type { IMultiSelectOption } from './components/inputs/MultiSelectInput/MultiSelectInput.ts';
7
9
  export { default as Radio } from './components/inputs/Radio/Radio.svelte';
8
10
  export { default as Toggle } from './components/inputs/Toggle/Toggle.svelte';
9
11
  export { default as Segmented } from './components/inputs/Segmented/Segmented.svelte';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { default as Button } from './components/Button/Button.svelte';
3
3
  export { default as Icon } from './components/Icon/Icon.svelte';
4
4
  export { default as TextInput } from './components/inputs/TextInput/TextInput.svelte';
5
+ export { default as TextInputSuggestion } from './components/inputs/TextInputSuggestion/TextInputSuggestion.svelte';
5
6
  export { default as TextareaInput } from './components/inputs/TextareaInput/TextareaInput.svelte';
6
7
  export { default as SelectInput } from './components/inputs/SelectInput/SelectInput.svelte';
7
8
  export { default as MultiSelectInput } from './components/inputs/MultiSelectInput/MultiSelectInput.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mertani-web-toolkit",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "homepage": "https://storybook.mertani.com/",
5
5
  "scripts": {
6
6
  "dev": "vite dev",