@synthaxai/ui 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +40 -220
- package/dist/data-display/DataTable/DataTable.svelte +14 -8
- package/dist/data-display/DataTable/DataTable.svelte.d.ts +64 -61
- package/dist/data-display/DataTable/DataTable.svelte.d.ts.map +1 -1
- package/dist/forms/DatePicker/DatePicker.svelte +1564 -0
- package/dist/forms/DatePicker/DatePicker.svelte.d.ts +129 -0
- package/dist/forms/DatePicker/DatePicker.svelte.d.ts.map +1 -0
- package/dist/forms/DatePicker/index.d.ts +8 -0
- package/dist/forms/DatePicker/index.d.ts.map +1 -0
- package/dist/forms/DatePicker/index.js +7 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte +1651 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts +127 -0
- package/dist/forms/MultiSelect/MultiSelect.svelte.d.ts.map +1 -0
- package/dist/forms/MultiSelect/index.d.ts +9 -0
- package/dist/forms/MultiSelect/index.d.ts.map +1 -0
- package/dist/forms/MultiSelect/index.js +7 -0
- package/dist/forms/SearchInput/SearchInput.svelte +1319 -0
- package/dist/forms/SearchInput/SearchInput.svelte.d.ts +148 -0
- package/dist/forms/SearchInput/SearchInput.svelte.d.ts.map +1 -0
- package/dist/forms/SearchInput/index.d.ts +9 -0
- package/dist/forms/SearchInput/index.d.ts.map +1 -0
- package/dist/forms/SearchInput/index.js +7 -0
- package/dist/forms/TextInput/TextInput.svelte +763 -122
- package/dist/forms/TextInput/TextInput.svelte.d.ts +98 -13
- package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -1
- package/dist/forms/TextInput/index.d.ts +7 -1
- package/dist/forms/TextInput/index.d.ts.map +1 -1
- package/dist/forms/TextInput/index.js +7 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +3 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/package.json +7 -7
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component TextInput
|
|
3
3
|
|
|
4
|
-
A fully accessible text input component with validation states,
|
|
5
|
-
|
|
4
|
+
A fully accessible, enhanced text input component with validation states,
|
|
5
|
+
input masking, clearable button, password toggle, prefix/suffix support,
|
|
6
|
+
and comprehensive ARIA support.
|
|
6
7
|
|
|
7
|
-
Designed for healthcare applications with proper error handling
|
|
8
|
-
|
|
8
|
+
Designed for healthcare applications with proper error handling,
|
|
9
|
+
clear visual feedback, and support for common healthcare data formats
|
|
10
|
+
like SSN, phone numbers, dates, NPI, and member IDs.
|
|
9
11
|
|
|
10
|
-
@example
|
|
12
|
+
@example Basic usage
|
|
11
13
|
<TextInput
|
|
12
14
|
id="patient-name"
|
|
13
15
|
label="Patient Name"
|
|
@@ -16,22 +18,85 @@
|
|
|
16
18
|
required
|
|
17
19
|
/>
|
|
18
20
|
|
|
19
|
-
@example
|
|
21
|
+
@example With masking (phone number)
|
|
22
|
+
<TextInput
|
|
23
|
+
label="Phone Number"
|
|
24
|
+
value={phone}
|
|
25
|
+
mask="(###) ###-####"
|
|
26
|
+
inputMode="tel"
|
|
27
|
+
placeholder="(555) 555-5555"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
@example Clearable with loading
|
|
20
31
|
<TextInput
|
|
21
|
-
id="member-id"
|
|
22
32
|
label="Member ID"
|
|
23
33
|
value={memberId}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
clearable
|
|
35
|
+
loading={isValidating}
|
|
36
|
+
hint="Enter your 12-digit member ID"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
@example Password with toggle
|
|
40
|
+
<TextInput
|
|
41
|
+
label="Password"
|
|
42
|
+
type="password"
|
|
43
|
+
value={password}
|
|
44
|
+
showPasswordToggle
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
@example With prefix/suffix
|
|
48
|
+
<TextInput
|
|
49
|
+
label="Amount"
|
|
50
|
+
value={amount}
|
|
51
|
+
prefix="$"
|
|
52
|
+
suffix=".00"
|
|
53
|
+
inputMode="decimal"
|
|
27
54
|
/>
|
|
28
55
|
-->
|
|
56
|
+
<script lang="ts" module>
|
|
57
|
+
/**
|
|
58
|
+
* Healthcare-specific input mask presets.
|
|
59
|
+
* Use these with the `mask` prop for consistent formatting.
|
|
60
|
+
*/
|
|
61
|
+
export const INPUT_MASKS = {
|
|
62
|
+
/** US Phone: (555) 555-5555 */
|
|
63
|
+
PHONE_US: '(###) ###-####',
|
|
64
|
+
/** US Phone with country code: +1 (555) 555-5555 */
|
|
65
|
+
PHONE_US_INTL: '+1 (###) ###-####',
|
|
66
|
+
/** Social Security Number: 123-45-6789 */
|
|
67
|
+
SSN: '###-##-####',
|
|
68
|
+
/** Date (US format): 01/15/1990 */
|
|
69
|
+
DATE_US: '##/##/####',
|
|
70
|
+
/** Date (ISO format): 1990-01-15 */
|
|
71
|
+
DATE_ISO: '####-##-##',
|
|
72
|
+
/** ZIP Code: 12345 */
|
|
73
|
+
ZIP: '#####',
|
|
74
|
+
/** ZIP+4: 12345-6789 */
|
|
75
|
+
ZIP_PLUS4: '#####-####',
|
|
76
|
+
/** National Provider Identifier: 1234567890 */
|
|
77
|
+
NPI: '##########',
|
|
78
|
+
/** 12-digit Member ID */
|
|
79
|
+
MEMBER_ID_12: '############',
|
|
80
|
+
/** Credit Card: 4111-1111-1111-1111 */
|
|
81
|
+
CREDIT_CARD: '####-####-####-####',
|
|
82
|
+
/** CVV: 123 */
|
|
83
|
+
CVV: '###',
|
|
84
|
+
/** Time 12hr: 12:30 */
|
|
85
|
+
TIME_12: '##:##',
|
|
86
|
+
/** Time 24hr: 14:30 */
|
|
87
|
+
TIME_24: '##:##',
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
export type InputMask = (typeof INPUT_MASKS)[keyof typeof INPUT_MASKS] | string;
|
|
91
|
+
</script>
|
|
92
|
+
|
|
29
93
|
<script lang="ts">
|
|
30
94
|
import type { Snippet } from 'svelte';
|
|
31
95
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
32
|
-
import { Check, AlertCircle, Info } from 'lucide-svelte';
|
|
96
|
+
import { Check, AlertCircle, Info, X, Eye, EyeOff, Loader2 } from 'lucide-svelte';
|
|
33
97
|
import { cn } from '../../utils/cn.js';
|
|
34
98
|
import { generateId } from '../../utils/keyboard.js';
|
|
99
|
+
import { announce, prefersReducedMotion } from '../../utils/accessibility.js';
|
|
35
100
|
import type { InputMode, AutoComplete } from '../../types/index.js';
|
|
36
101
|
|
|
37
102
|
type InputSize = 'sm' | 'md' | 'lg';
|
|
@@ -46,7 +111,7 @@
|
|
|
46
111
|
type?: InputType;
|
|
47
112
|
/** Label text for the input */
|
|
48
113
|
label: string;
|
|
49
|
-
/** Current value */
|
|
114
|
+
/** Current value (bindable) */
|
|
50
115
|
value?: string;
|
|
51
116
|
/** Placeholder text */
|
|
52
117
|
placeholder?: string;
|
|
@@ -84,14 +149,45 @@
|
|
|
84
149
|
hideLabel?: boolean;
|
|
85
150
|
/** Additional CSS classes for the wrapper */
|
|
86
151
|
class?: string;
|
|
87
|
-
/** Format function to transform input value */
|
|
152
|
+
/** Format function to transform input value (legacy, prefer mask) */
|
|
88
153
|
formatFn?: (value: string) => string;
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Enhanced Features
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Input mask pattern. Use # for digits, A for letters, * for alphanumeric.
|
|
161
|
+
* Example: "###-##-####" for SSN, "(###) ###-####" for phone
|
|
162
|
+
*/
|
|
163
|
+
mask?: InputMask;
|
|
164
|
+
/** Character to show for unfilled mask positions (default: none) */
|
|
165
|
+
maskPlaceholder?: string;
|
|
166
|
+
/** Whether to include mask literals in the value (default: false, returns only user input) */
|
|
167
|
+
maskIncludeLiterals?: boolean;
|
|
168
|
+
/** Show clear button when input has value */
|
|
169
|
+
clearable?: boolean;
|
|
170
|
+
/** Show password visibility toggle (only for type="password") */
|
|
171
|
+
showPasswordToggle?: boolean;
|
|
172
|
+
/** Static text to display before the input */
|
|
173
|
+
prefix?: string;
|
|
174
|
+
/** Static text to display after the input */
|
|
175
|
+
suffix?: string;
|
|
176
|
+
/** Show loading spinner */
|
|
177
|
+
loading?: boolean;
|
|
178
|
+
/** Debounce delay for oninput (in ms) */
|
|
179
|
+
debounce?: number;
|
|
180
|
+
/** Called with debounced value */
|
|
181
|
+
ondebounce?: (value: string) => void;
|
|
182
|
+
|
|
89
183
|
/** Input handler */
|
|
90
184
|
oninput?: (event: Event) => void;
|
|
91
185
|
/** Blur handler */
|
|
92
186
|
onblur?: (event: FocusEvent) => void;
|
|
93
187
|
/** Focus handler */
|
|
94
188
|
onfocus?: (event: FocusEvent) => void;
|
|
189
|
+
/** Called when clear button is clicked */
|
|
190
|
+
onclear?: () => void;
|
|
95
191
|
/** Test ID for e2e testing (Playwright, Cypress) */
|
|
96
192
|
testId?: string;
|
|
97
193
|
/** Show validation state immediately without waiting for interaction */
|
|
@@ -103,7 +199,7 @@
|
|
|
103
199
|
name,
|
|
104
200
|
type = 'text',
|
|
105
201
|
label,
|
|
106
|
-
value = '',
|
|
202
|
+
value = $bindable(''),
|
|
107
203
|
placeholder = '',
|
|
108
204
|
required = false,
|
|
109
205
|
disabled = false,
|
|
@@ -123,34 +219,249 @@
|
|
|
123
219
|
hideLabel = false,
|
|
124
220
|
class: className = '',
|
|
125
221
|
formatFn,
|
|
222
|
+
// Enhanced features
|
|
223
|
+
mask,
|
|
224
|
+
maskPlaceholder,
|
|
225
|
+
maskIncludeLiterals = false,
|
|
226
|
+
clearable = false,
|
|
227
|
+
showPasswordToggle = false,
|
|
228
|
+
prefix,
|
|
229
|
+
suffix,
|
|
230
|
+
loading = false,
|
|
231
|
+
debounce = 0,
|
|
232
|
+
ondebounce,
|
|
126
233
|
oninput,
|
|
127
234
|
onblur,
|
|
128
235
|
onfocus,
|
|
236
|
+
onclear,
|
|
129
237
|
testId,
|
|
130
238
|
validateOnMount = false,
|
|
131
239
|
...restProps
|
|
132
240
|
}: Props = $props();
|
|
133
241
|
|
|
134
|
-
//
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// State
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
let inputRef: HTMLInputElement | undefined = $state();
|
|
135
247
|
let isFocused = $state(false);
|
|
136
248
|
let hasInteracted = $state(false);
|
|
249
|
+
let passwordVisible = $state(false);
|
|
250
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Derived State
|
|
254
|
+
// ============================================================================
|
|
137
255
|
|
|
138
|
-
//
|
|
139
|
-
// Validation shows: after interaction OR if validateOnMount is enabled
|
|
256
|
+
// Validation display logic
|
|
140
257
|
const shouldValidate = $derived(hasInteracted || validateOnMount);
|
|
141
258
|
const showError = $derived(shouldValidate && !!error);
|
|
142
259
|
const showSuccess = $derived(shouldValidate && !error && !!value && !!successMessage);
|
|
143
260
|
const isValid = $derived(shouldValidate && !error && !!value && !!successMessage);
|
|
261
|
+
|
|
262
|
+
// IDs for accessibility
|
|
144
263
|
const errorId = $derived(`${id}-error`);
|
|
145
264
|
const hintId = $derived(`${id}-hint`);
|
|
146
265
|
|
|
266
|
+
// Determine effective input type
|
|
267
|
+
const effectiveType = $derived(
|
|
268
|
+
type === 'password' && passwordVisible ? 'text' : type
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Determine if we need right-side space for icons/buttons
|
|
272
|
+
const hasRightContent = $derived(
|
|
273
|
+
loading ||
|
|
274
|
+
showError ||
|
|
275
|
+
isValid ||
|
|
276
|
+
(clearable && value && !disabled && !readonly) ||
|
|
277
|
+
(showPasswordToggle && type === 'password') ||
|
|
278
|
+
iconRight
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Count right icons for padding calculation
|
|
282
|
+
const rightIconCount = $derived.by(() => {
|
|
283
|
+
let count = 0;
|
|
284
|
+
if (loading) count++;
|
|
285
|
+
else if (showError || isValid) count++;
|
|
286
|
+
if (clearable && value && !disabled && !readonly) count++;
|
|
287
|
+
if (showPasswordToggle && type === 'password') count++;
|
|
288
|
+
if (iconRight && !loading && !showError && !isValid) count++;
|
|
289
|
+
return count;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Calculate if mask is complete
|
|
293
|
+
const maskComplete = $derived.by(() => {
|
|
294
|
+
if (!mask) return true;
|
|
295
|
+
const requiredDigits = (mask.match(/#/g) || []).length;
|
|
296
|
+
const requiredLetters = (mask.match(/A/g) || []).length;
|
|
297
|
+
const requiredAny = (mask.match(/\*/g) || []).length;
|
|
298
|
+
const rawValue = getRawValue(value);
|
|
299
|
+
return rawValue.length >= requiredDigits + requiredLetters + requiredAny;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Build aria-describedby
|
|
303
|
+
const ariaDescribedBy = $derived(
|
|
304
|
+
[error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// Mask Logic
|
|
309
|
+
// ============================================================================
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if character is valid for mask position
|
|
313
|
+
*/
|
|
314
|
+
function isValidMaskChar(char: string, maskChar: string): boolean {
|
|
315
|
+
if (maskChar === '#') return /\d/.test(char);
|
|
316
|
+
if (maskChar === 'A') return /[a-zA-Z]/.test(char);
|
|
317
|
+
if (maskChar === '*') return /[a-zA-Z0-9]/.test(char);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract raw characters from input based on mask type
|
|
323
|
+
*/
|
|
324
|
+
function extractRawChars(input: string): string {
|
|
325
|
+
if (!mask) return input;
|
|
326
|
+
|
|
327
|
+
// Determine what character types the mask expects
|
|
328
|
+
const hasDigitSlots = mask.includes('#');
|
|
329
|
+
const hasLetterSlots = mask.includes('A');
|
|
330
|
+
const hasAnySlots = mask.includes('*');
|
|
331
|
+
|
|
332
|
+
let result = '';
|
|
333
|
+
for (const char of input) {
|
|
334
|
+
if (hasDigitSlots && /\d/.test(char)) {
|
|
335
|
+
result += char;
|
|
336
|
+
} else if (hasLetterSlots && /[a-zA-Z]/.test(char)) {
|
|
337
|
+
result += char;
|
|
338
|
+
} else if (hasAnySlots && /[a-zA-Z0-9]/.test(char)) {
|
|
339
|
+
result += char;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get raw value without mask literals (for external use)
|
|
347
|
+
*/
|
|
348
|
+
function getRawValue(maskedValue: string): string {
|
|
349
|
+
if (!mask) return maskedValue;
|
|
350
|
+
return extractRawChars(maskedValue);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Apply mask to raw input characters
|
|
355
|
+
*/
|
|
356
|
+
function applyMask(input: string): string {
|
|
357
|
+
if (!mask) return input;
|
|
358
|
+
|
|
359
|
+
// Extract only valid characters
|
|
360
|
+
const rawChars = extractRawChars(input);
|
|
361
|
+
|
|
362
|
+
// Build masked output
|
|
363
|
+
let result = '';
|
|
364
|
+
let charIndex = 0;
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < mask.length && charIndex < rawChars.length; i++) {
|
|
367
|
+
const maskChar = mask[i];
|
|
368
|
+
if (maskChar === '#' || maskChar === 'A' || maskChar === '*') {
|
|
369
|
+
// This is a placeholder slot - fill with next raw char
|
|
370
|
+
const rawChar = rawChars[charIndex];
|
|
371
|
+
if (isValidMaskChar(rawChar, maskChar)) {
|
|
372
|
+
result += rawChar;
|
|
373
|
+
charIndex++;
|
|
374
|
+
} else {
|
|
375
|
+
// Character doesn't match expected type, skip it
|
|
376
|
+
charIndex++;
|
|
377
|
+
i--; // Try this mask position again
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// This is a literal character - add it
|
|
381
|
+
result += maskChar;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add mask placeholder for remaining positions if specified and focused
|
|
386
|
+
if (maskPlaceholder && isFocused) {
|
|
387
|
+
for (let i = result.length; i < mask.length; i++) {
|
|
388
|
+
const maskChar = mask[i];
|
|
389
|
+
if (maskChar === '#' || maskChar === 'A' || maskChar === '*') {
|
|
390
|
+
result += maskPlaceholder;
|
|
391
|
+
} else {
|
|
392
|
+
result += maskChar;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Calculate cursor position after mask application
|
|
402
|
+
*/
|
|
403
|
+
function calculateCursorPosition(rawLength: number): number {
|
|
404
|
+
if (!mask) return rawLength;
|
|
405
|
+
|
|
406
|
+
let position = 0;
|
|
407
|
+
let rawCount = 0;
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < mask.length && rawCount < rawLength; i++) {
|
|
410
|
+
const maskChar = mask[i];
|
|
411
|
+
position++;
|
|
412
|
+
if (maskChar === '#' || maskChar === 'A' || maskChar === '*') {
|
|
413
|
+
rawCount++;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return position;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Event Handlers
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
147
424
|
function handleInput(e: Event) {
|
|
148
425
|
const target = e.target as HTMLInputElement;
|
|
149
426
|
let newValue = target.value;
|
|
150
427
|
|
|
151
|
-
if
|
|
152
|
-
|
|
428
|
+
// Apply mask if present
|
|
429
|
+
if (mask) {
|
|
430
|
+
// Apply mask to the input value
|
|
431
|
+
newValue = applyMask(newValue);
|
|
432
|
+
const rawValue = getRawValue(newValue);
|
|
433
|
+
|
|
434
|
+
// Calculate cursor position based on raw value length
|
|
435
|
+
const newCursorPos = calculateCursorPosition(rawValue.length);
|
|
436
|
+
|
|
153
437
|
target.value = newValue;
|
|
438
|
+
|
|
439
|
+
// Restore cursor position
|
|
440
|
+
requestAnimationFrame(() => {
|
|
441
|
+
if (target && document.activeElement === target) {
|
|
442
|
+
target.setSelectionRange(newCursorPos, newCursorPos);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Update value (raw or with literals based on setting)
|
|
447
|
+
value = maskIncludeLiterals ? newValue : rawValue;
|
|
448
|
+
} else {
|
|
449
|
+
// Apply legacy format function
|
|
450
|
+
if (formatFn) {
|
|
451
|
+
newValue = formatFn(newValue);
|
|
452
|
+
target.value = newValue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update value
|
|
456
|
+
value = newValue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle debounce
|
|
460
|
+
if (debounce > 0 && ondebounce) {
|
|
461
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
462
|
+
debounceTimer = setTimeout(() => {
|
|
463
|
+
ondebounce(value);
|
|
464
|
+
}, debounce);
|
|
154
465
|
}
|
|
155
466
|
|
|
156
467
|
oninput?.(e);
|
|
@@ -159,18 +470,88 @@
|
|
|
159
470
|
function handleBlur(e: FocusEvent) {
|
|
160
471
|
isFocused = false;
|
|
161
472
|
hasInteracted = true;
|
|
473
|
+
|
|
474
|
+
// Clear debounce and fire immediately
|
|
475
|
+
if (debounceTimer && ondebounce) {
|
|
476
|
+
clearTimeout(debounceTimer);
|
|
477
|
+
debounceTimer = null;
|
|
478
|
+
ondebounce(value);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Remove mask placeholder on blur
|
|
482
|
+
if (mask && maskPlaceholder && inputRef) {
|
|
483
|
+
const rawValue = getRawValue(value);
|
|
484
|
+
if (rawValue) {
|
|
485
|
+
inputRef.value = applyMask(rawValue);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
162
489
|
onblur?.(e);
|
|
163
490
|
}
|
|
164
491
|
|
|
165
492
|
function handleFocus(e: FocusEvent) {
|
|
166
493
|
isFocused = true;
|
|
494
|
+
|
|
495
|
+
// Show mask placeholder on focus
|
|
496
|
+
if (mask && maskPlaceholder && inputRef) {
|
|
497
|
+
const currentRaw = getRawValue(inputRef.value);
|
|
498
|
+
inputRef.value = applyMask(currentRaw);
|
|
499
|
+
}
|
|
500
|
+
|
|
167
501
|
onfocus?.(e);
|
|
168
502
|
}
|
|
169
503
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
504
|
+
function handleClear() {
|
|
505
|
+
value = '';
|
|
506
|
+
if (inputRef) {
|
|
507
|
+
inputRef.value = '';
|
|
508
|
+
inputRef.focus();
|
|
509
|
+
}
|
|
510
|
+
announce('Input cleared', 'polite');
|
|
511
|
+
onclear?.();
|
|
512
|
+
|
|
513
|
+
// Trigger debounce callback with empty value
|
|
514
|
+
if (ondebounce) {
|
|
515
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
516
|
+
ondebounce('');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function togglePasswordVisibility() {
|
|
521
|
+
passwordVisible = !passwordVisible;
|
|
522
|
+
announce(passwordVisible ? 'Password shown' : 'Password hidden', 'polite');
|
|
523
|
+
// Keep focus on input
|
|
524
|
+
inputRef?.focus();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
528
|
+
// Handle Escape to clear when clearable
|
|
529
|
+
if (e.key === 'Escape' && clearable && value && !disabled && !readonly) {
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
handleClear();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ============================================================================
|
|
536
|
+
// Effects
|
|
537
|
+
// ============================================================================
|
|
538
|
+
|
|
539
|
+
// Apply initial mask formatting
|
|
540
|
+
$effect(() => {
|
|
541
|
+
if (mask && inputRef && value) {
|
|
542
|
+
const maskedValue = applyMask(maskIncludeLiterals ? value : getRawValue(value) || value);
|
|
543
|
+
if (inputRef.value !== maskedValue) {
|
|
544
|
+
inputRef.value = maskedValue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Cleanup debounce timer
|
|
550
|
+
$effect(() => {
|
|
551
|
+
return () => {
|
|
552
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
553
|
+
};
|
|
554
|
+
});
|
|
174
555
|
</script>
|
|
175
556
|
|
|
176
557
|
<div class={cn('input-wrapper', `input-${size}`, disabled && 'input-disabled', className)}>
|
|
@@ -188,7 +569,23 @@
|
|
|
188
569
|
</label>
|
|
189
570
|
|
|
190
571
|
<!-- Input wrapper -->
|
|
191
|
-
<div
|
|
572
|
+
<div
|
|
573
|
+
class={cn(
|
|
574
|
+
'input-field-wrapper',
|
|
575
|
+
isFocused && 'input-field-wrapper-focused',
|
|
576
|
+
showError && 'input-field-wrapper-error',
|
|
577
|
+
isValid && 'input-field-wrapper-valid',
|
|
578
|
+
disabled && 'input-field-wrapper-disabled'
|
|
579
|
+
)}
|
|
580
|
+
>
|
|
581
|
+
<!-- Prefix (inside input container) -->
|
|
582
|
+
{#if prefix}
|
|
583
|
+
<span class={cn('input-prefix', isFocused && 'input-prefix-focused')} aria-hidden="true">
|
|
584
|
+
{prefix}
|
|
585
|
+
</span>
|
|
586
|
+
{/if}
|
|
587
|
+
|
|
588
|
+
<!-- Left icon -->
|
|
192
589
|
{#if iconLeft}
|
|
193
590
|
<span class={cn('input-icon-left', isFocused && 'input-icon-focused')} aria-hidden="true">
|
|
194
591
|
{@render iconLeft()}
|
|
@@ -196,10 +593,11 @@
|
|
|
196
593
|
{/if}
|
|
197
594
|
|
|
198
595
|
<input
|
|
596
|
+
bind:this={inputRef}
|
|
199
597
|
{id}
|
|
200
598
|
{name}
|
|
201
|
-
{
|
|
202
|
-
{value}
|
|
599
|
+
type={effectiveType}
|
|
600
|
+
value={mask ? applyMask(maskIncludeLiterals ? value : value) : value}
|
|
203
601
|
{placeholder}
|
|
204
602
|
{disabled}
|
|
205
603
|
{readonly}
|
|
@@ -207,7 +605,7 @@
|
|
|
207
605
|
{pattern}
|
|
208
606
|
maxlength={maxLength}
|
|
209
607
|
minlength={minLength}
|
|
210
|
-
inputmode={inputMode}
|
|
608
|
+
inputmode={mask ? 'numeric' : inputMode}
|
|
211
609
|
{autocomplete}
|
|
212
610
|
aria-invalid={showError}
|
|
213
611
|
aria-describedby={ariaDescribedBy}
|
|
@@ -215,29 +613,79 @@
|
|
|
215
613
|
data-testid={testId}
|
|
216
614
|
class={cn(
|
|
217
615
|
'input-field',
|
|
616
|
+
prefix && 'input-has-prefix',
|
|
617
|
+
suffix && 'input-has-suffix',
|
|
218
618
|
iconLeft && 'input-has-icon-left',
|
|
219
|
-
(
|
|
220
|
-
|
|
221
|
-
|
|
619
|
+
(prefix && iconLeft) && 'input-has-prefix-and-icon',
|
|
620
|
+
hasRightContent && 'input-has-icon-right',
|
|
621
|
+
rightIconCount > 1 && 'input-has-multiple-right',
|
|
622
|
+
rightIconCount > 2 && 'input-has-triple-right',
|
|
623
|
+
(suffix && hasRightContent) && 'input-has-suffix-and-right'
|
|
222
624
|
)}
|
|
223
625
|
oninput={handleInput}
|
|
224
626
|
onblur={handleBlur}
|
|
225
627
|
onfocus={handleFocus}
|
|
628
|
+
onkeydown={handleKeyDown}
|
|
226
629
|
{...restProps}
|
|
227
630
|
/>
|
|
228
631
|
|
|
229
|
-
<!--
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
632
|
+
<!-- Right side icons and buttons -->
|
|
633
|
+
<div class={cn('input-right-content', suffix && 'input-right-content-before-suffix')}>
|
|
634
|
+
<!-- Loading spinner -->
|
|
635
|
+
{#if loading}
|
|
636
|
+
<span class="input-icon-loading" aria-hidden="true">
|
|
637
|
+
<Loader2 size={size === 'sm' ? 14 : 16} class="input-spinner" />
|
|
638
|
+
</span>
|
|
639
|
+
{:else if showError}
|
|
640
|
+
<span class="input-icon-right input-icon-error" aria-hidden="true">
|
|
641
|
+
<AlertCircle size={size === 'sm' ? 14 : 16} />
|
|
642
|
+
</span>
|
|
643
|
+
{:else if isValid}
|
|
644
|
+
<span class="input-icon-right input-icon-success" aria-hidden="true">
|
|
645
|
+
<Check size={size === 'sm' ? 14 : 16} />
|
|
646
|
+
</span>
|
|
647
|
+
{:else if iconRight}
|
|
648
|
+
<span class="input-icon-right" aria-hidden="true">
|
|
649
|
+
{@render iconRight()}
|
|
650
|
+
</span>
|
|
651
|
+
{/if}
|
|
652
|
+
|
|
653
|
+
<!-- Clear button -->
|
|
654
|
+
{#if clearable && value && !disabled && !readonly}
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
class="input-clear-button"
|
|
658
|
+
onclick={handleClear}
|
|
659
|
+
aria-label="Clear input"
|
|
660
|
+
tabindex={-1}
|
|
661
|
+
>
|
|
662
|
+
<X size={size === 'sm' ? 14 : 16} />
|
|
663
|
+
</button>
|
|
664
|
+
{/if}
|
|
665
|
+
|
|
666
|
+
<!-- Password toggle -->
|
|
667
|
+
{#if showPasswordToggle && type === 'password'}
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
670
|
+
class="input-password-toggle"
|
|
671
|
+
onclick={togglePasswordVisibility}
|
|
672
|
+
aria-label={passwordVisible ? 'Hide password' : 'Show password'}
|
|
673
|
+
aria-pressed={passwordVisible}
|
|
674
|
+
tabindex={-1}
|
|
675
|
+
>
|
|
676
|
+
{#if passwordVisible}
|
|
677
|
+
<EyeOff size={size === 'sm' ? 14 : 16} />
|
|
678
|
+
{:else}
|
|
679
|
+
<Eye size={size === 'sm' ? 14 : 16} />
|
|
680
|
+
{/if}
|
|
681
|
+
</button>
|
|
682
|
+
{/if}
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
<!-- Suffix (inside input container) -->
|
|
686
|
+
{#if suffix}
|
|
687
|
+
<span class={cn('input-suffix', isFocused && 'input-suffix-focused')} aria-hidden="true">
|
|
688
|
+
{suffix}
|
|
241
689
|
</span>
|
|
242
690
|
{/if}
|
|
243
691
|
</div>
|
|
@@ -322,156 +770,250 @@
|
|
|
322
770
|
}
|
|
323
771
|
|
|
324
772
|
/* ========================================
|
|
325
|
-
FIELD WRAPPER
|
|
773
|
+
FIELD WRAPPER (Unified container)
|
|
326
774
|
======================================== */
|
|
327
775
|
.input-field-wrapper {
|
|
328
776
|
position: relative;
|
|
329
777
|
display: flex;
|
|
330
778
|
align-items: center;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* ========================================
|
|
334
|
-
INPUT FIELD
|
|
335
|
-
======================================== */
|
|
336
|
-
.input-field {
|
|
337
|
-
width: 100%;
|
|
338
779
|
border: 2px solid var(--ui-border-default);
|
|
339
780
|
border-radius: 0.5rem;
|
|
340
781
|
background: var(--ui-bg-secondary);
|
|
341
|
-
color: var(--ui-text-primary);
|
|
342
782
|
transition: all 0.2s ease;
|
|
343
783
|
}
|
|
344
784
|
|
|
345
|
-
.input-field
|
|
346
|
-
color: var(--ui-text-tertiary);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.input-field:focus-visible {
|
|
350
|
-
outline: none;
|
|
785
|
+
.input-field-wrapper-focused {
|
|
351
786
|
background: var(--ui-bg-primary);
|
|
352
787
|
border-color: rgb(var(--ui-color-primary));
|
|
353
788
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary) / 0.4);
|
|
354
789
|
}
|
|
355
790
|
|
|
356
|
-
:global([data-theme='dark']) .input-field
|
|
791
|
+
:global([data-theme='dark']) .input-field-wrapper-focused {
|
|
357
792
|
border-color: rgb(var(--ui-color-primary-light));
|
|
358
793
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
359
794
|
}
|
|
360
795
|
|
|
361
796
|
@media (prefers-color-scheme: dark) {
|
|
362
|
-
:global(:root:not([data-theme='light'])) .input-field
|
|
797
|
+
:global(:root:not([data-theme='light'])) .input-field-wrapper-focused {
|
|
363
798
|
border-color: rgb(var(--ui-color-primary-light));
|
|
364
799
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-primary-light) / 0.5);
|
|
365
800
|
}
|
|
366
801
|
}
|
|
367
802
|
|
|
368
|
-
.input-field
|
|
369
|
-
cursor: not-allowed;
|
|
370
|
-
background: var(--ui-bg-tertiary);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.input-field:read-only {
|
|
374
|
-
background: var(--ui-bg-tertiary);
|
|
375
|
-
cursor: default;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/* Size variants */
|
|
379
|
-
.input-sm .input-field {
|
|
380
|
-
height: 2rem;
|
|
381
|
-
padding: 0 0.75rem;
|
|
382
|
-
font-size: 0.875rem;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
.input-md .input-field {
|
|
386
|
-
height: 2.5rem;
|
|
387
|
-
padding: 0 0.75rem;
|
|
388
|
-
font-size: 0.875rem;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.input-lg .input-field {
|
|
392
|
-
height: 3rem;
|
|
393
|
-
padding: 0 1rem;
|
|
394
|
-
font-size: 1rem;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/* Icon padding adjustments - use !important to override size-specific padding */
|
|
398
|
-
.input-field.input-has-icon-left {
|
|
399
|
-
padding-left: 2.25rem !important;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
.input-field.input-has-icon-right {
|
|
403
|
-
padding-right: 2.25rem !important;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/* Error state */
|
|
407
|
-
.input-error {
|
|
803
|
+
.input-field-wrapper-error {
|
|
408
804
|
border-color: rgb(var(--ui-color-error));
|
|
409
805
|
}
|
|
410
806
|
|
|
411
|
-
.input-error
|
|
807
|
+
.input-field-wrapper-focused.input-field-wrapper-error {
|
|
412
808
|
border-color: rgb(var(--ui-color-error));
|
|
413
809
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-error) / 0.4);
|
|
414
810
|
}
|
|
415
811
|
|
|
416
|
-
:global([data-theme='dark']) .input-error {
|
|
812
|
+
:global([data-theme='dark']) .input-field-wrapper-error {
|
|
417
813
|
border-color: rgb(var(--ui-color-error-light));
|
|
418
814
|
}
|
|
419
815
|
|
|
420
|
-
:global([data-theme='dark']) .input-error
|
|
816
|
+
:global([data-theme='dark']) .input-field-wrapper-focused.input-field-wrapper-error {
|
|
421
817
|
border-color: rgb(var(--ui-color-error-light));
|
|
422
818
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-error-light) / 0.5);
|
|
423
819
|
}
|
|
424
820
|
|
|
425
821
|
@media (prefers-color-scheme: dark) {
|
|
426
|
-
:global(:root:not([data-theme='light'])) .input-error {
|
|
822
|
+
:global(:root:not([data-theme='light'])) .input-field-wrapper-error {
|
|
427
823
|
border-color: rgb(var(--ui-color-error-light));
|
|
428
824
|
}
|
|
429
825
|
|
|
430
|
-
:global(:root:not([data-theme='light'])) .input-error
|
|
826
|
+
:global(:root:not([data-theme='light'])) .input-field-wrapper-focused.input-field-wrapper-error {
|
|
431
827
|
border-color: rgb(var(--ui-color-error-light));
|
|
432
828
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-error-light) / 0.5);
|
|
433
829
|
}
|
|
434
830
|
}
|
|
435
831
|
|
|
436
|
-
|
|
437
|
-
.input-valid {
|
|
832
|
+
.input-field-wrapper-valid {
|
|
438
833
|
border-color: rgb(var(--ui-color-success));
|
|
439
834
|
}
|
|
440
835
|
|
|
441
|
-
.input-valid
|
|
836
|
+
.input-field-wrapper-focused.input-field-wrapper-valid {
|
|
442
837
|
border-color: rgb(var(--ui-color-success));
|
|
443
838
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-success) / 0.4);
|
|
444
839
|
}
|
|
445
840
|
|
|
446
|
-
:global([data-theme='dark']) .input-valid {
|
|
841
|
+
:global([data-theme='dark']) .input-field-wrapper-valid {
|
|
447
842
|
border-color: rgb(var(--ui-color-success-light));
|
|
448
843
|
}
|
|
449
844
|
|
|
450
|
-
:global([data-theme='dark']) .input-valid
|
|
845
|
+
:global([data-theme='dark']) .input-field-wrapper-focused.input-field-wrapper-valid {
|
|
451
846
|
border-color: rgb(var(--ui-color-success-light));
|
|
452
847
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-success-light) / 0.5);
|
|
453
848
|
}
|
|
454
849
|
|
|
455
850
|
@media (prefers-color-scheme: dark) {
|
|
456
|
-
:global(:root:not([data-theme='light'])) .input-valid {
|
|
851
|
+
:global(:root:not([data-theme='light'])) .input-field-wrapper-valid {
|
|
457
852
|
border-color: rgb(var(--ui-color-success-light));
|
|
458
853
|
}
|
|
459
854
|
|
|
460
|
-
:global(:root:not([data-theme='light'])) .input-valid
|
|
855
|
+
:global(:root:not([data-theme='light'])) .input-field-wrapper-focused.input-field-wrapper-valid {
|
|
461
856
|
border-color: rgb(var(--ui-color-success-light));
|
|
462
857
|
box-shadow: 0 0 0 3px rgb(var(--ui-color-success-light) / 0.5);
|
|
463
858
|
}
|
|
464
859
|
}
|
|
465
860
|
|
|
861
|
+
.input-field-wrapper-disabled {
|
|
862
|
+
opacity: 0.6;
|
|
863
|
+
cursor: not-allowed;
|
|
864
|
+
background: var(--ui-bg-tertiary);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* ========================================
|
|
868
|
+
PREFIX & SUFFIX (Inside container)
|
|
869
|
+
======================================== */
|
|
870
|
+
.input-prefix,
|
|
871
|
+
.input-suffix {
|
|
872
|
+
display: flex;
|
|
873
|
+
align-items: center;
|
|
874
|
+
flex-shrink: 0;
|
|
875
|
+
padding: 0 0.75rem;
|
|
876
|
+
color: var(--ui-text-tertiary);
|
|
877
|
+
font-size: 0.875rem;
|
|
878
|
+
font-weight: 500;
|
|
879
|
+
white-space: nowrap;
|
|
880
|
+
pointer-events: none;
|
|
881
|
+
user-select: none;
|
|
882
|
+
transition: color 0.2s ease;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.input-prefix {
|
|
886
|
+
padding-right: 0.375rem;
|
|
887
|
+
border-right: 1px solid var(--ui-border-default);
|
|
888
|
+
margin-right: 0.5rem;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.input-suffix {
|
|
892
|
+
padding-left: 0.375rem;
|
|
893
|
+
border-left: 1px solid var(--ui-border-default);
|
|
894
|
+
margin-left: 0.5rem;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.input-prefix-focused,
|
|
898
|
+
.input-suffix-focused {
|
|
899
|
+
color: var(--ui-text-secondary);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.input-sm .input-prefix,
|
|
903
|
+
.input-sm .input-suffix {
|
|
904
|
+
padding: 0 0.5rem;
|
|
905
|
+
font-size: 0.8125rem;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.input-sm .input-prefix {
|
|
909
|
+
padding-right: 0.25rem;
|
|
910
|
+
margin-right: 0.375rem;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.input-sm .input-suffix {
|
|
914
|
+
padding-left: 0.25rem;
|
|
915
|
+
margin-left: 0.375rem;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.input-lg .input-prefix,
|
|
919
|
+
.input-lg .input-suffix {
|
|
920
|
+
padding: 0 1rem;
|
|
921
|
+
font-size: 1rem;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.input-lg .input-prefix {
|
|
925
|
+
padding-right: 0.5rem;
|
|
926
|
+
margin-right: 0.625rem;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.input-lg .input-suffix {
|
|
930
|
+
padding-left: 0.5rem;
|
|
931
|
+
margin-left: 0.625rem;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/* ========================================
|
|
935
|
+
INPUT FIELD (Inside unified container)
|
|
936
|
+
======================================== */
|
|
937
|
+
.input-field {
|
|
938
|
+
flex: 1;
|
|
939
|
+
min-width: 0;
|
|
940
|
+
border: none;
|
|
941
|
+
border-radius: 0;
|
|
942
|
+
background: transparent;
|
|
943
|
+
color: var(--ui-text-primary);
|
|
944
|
+
outline: none;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.input-field::placeholder {
|
|
948
|
+
color: var(--ui-text-tertiary);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.input-field:focus-visible {
|
|
952
|
+
outline: none;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.input-field:disabled {
|
|
956
|
+
cursor: not-allowed;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.input-field:read-only {
|
|
960
|
+
cursor: default;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* Size variants - now applies to wrapper */
|
|
964
|
+
.input-sm .input-field-wrapper {
|
|
965
|
+
min-height: 2rem;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.input-md .input-field-wrapper {
|
|
969
|
+
min-height: 2.5rem;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.input-lg .input-field-wrapper {
|
|
973
|
+
min-height: 3rem;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.input-sm .input-field {
|
|
977
|
+
padding: 0 0.5rem;
|
|
978
|
+
font-size: 0.875rem;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.input-md .input-field {
|
|
982
|
+
padding: 0 0.625rem;
|
|
983
|
+
font-size: 0.875rem;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.input-lg .input-field {
|
|
987
|
+
padding: 0 0.75rem;
|
|
988
|
+
font-size: 1rem;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/* Padding adjustments - input is now flex child */
|
|
992
|
+
.input-field.input-has-prefix {
|
|
993
|
+
padding-left: 0.25rem !important;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.input-field.input-has-suffix {
|
|
997
|
+
padding-right: 0.25rem !important;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.input-field.input-has-icon-left {
|
|
1001
|
+
padding-left: 0.25rem !important;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.input-field.input-has-prefix-and-icon {
|
|
1005
|
+
padding-left: 0.25rem !important;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
466
1008
|
/* ========================================
|
|
467
1009
|
ICONS
|
|
468
1010
|
======================================== */
|
|
469
1011
|
.input-icon-left {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1012
|
+
display: flex;
|
|
1013
|
+
align-items: center;
|
|
1014
|
+
justify-content: center;
|
|
1015
|
+
flex-shrink: 0;
|
|
1016
|
+
padding-left: 0.75rem;
|
|
475
1017
|
pointer-events: none;
|
|
476
1018
|
color: var(--ui-text-tertiary);
|
|
477
1019
|
transition: color 0.2s ease;
|
|
@@ -491,14 +1033,25 @@
|
|
|
491
1033
|
}
|
|
492
1034
|
}
|
|
493
1035
|
|
|
1036
|
+
/* Right content container */
|
|
1037
|
+
.input-right-content {
|
|
1038
|
+
display: flex;
|
|
1039
|
+
align-items: center;
|
|
1040
|
+
gap: 0.25rem;
|
|
1041
|
+
flex-shrink: 0;
|
|
1042
|
+
padding-right: 0.5rem;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.input-right-content-before-suffix {
|
|
1046
|
+
padding-right: 0;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
494
1049
|
.input-icon-right {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
transform: translateY(-50%);
|
|
499
|
-
z-index: 1;
|
|
500
|
-
pointer-events: none;
|
|
1050
|
+
display: flex;
|
|
1051
|
+
align-items: center;
|
|
1052
|
+
justify-content: center;
|
|
501
1053
|
color: var(--ui-text-tertiary);
|
|
1054
|
+
pointer-events: none;
|
|
502
1055
|
}
|
|
503
1056
|
|
|
504
1057
|
.input-icon-error {
|
|
@@ -529,6 +1082,88 @@
|
|
|
529
1082
|
}
|
|
530
1083
|
}
|
|
531
1084
|
|
|
1085
|
+
/* Loading spinner */
|
|
1086
|
+
.input-icon-loading {
|
|
1087
|
+
display: flex;
|
|
1088
|
+
align-items: center;
|
|
1089
|
+
justify-content: center;
|
|
1090
|
+
color: rgb(var(--ui-color-primary));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
:global([data-theme='dark']) .input-icon-loading {
|
|
1094
|
+
color: rgb(var(--ui-color-primary-light));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
@media (prefers-color-scheme: dark) {
|
|
1098
|
+
:global(:root:not([data-theme='light'])) .input-icon-loading {
|
|
1099
|
+
color: rgb(var(--ui-color-primary-light));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.input-spinner {
|
|
1104
|
+
animation: spin 1s linear infinite;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
@keyframes spin {
|
|
1108
|
+
from { transform: rotate(0deg); }
|
|
1109
|
+
to { transform: rotate(360deg); }
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1113
|
+
.input-spinner {
|
|
1114
|
+
animation: none;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/* ========================================
|
|
1119
|
+
INTERACTIVE BUTTONS (Clear, Password Toggle)
|
|
1120
|
+
======================================== */
|
|
1121
|
+
.input-clear-button,
|
|
1122
|
+
.input-password-toggle {
|
|
1123
|
+
display: flex;
|
|
1124
|
+
align-items: center;
|
|
1125
|
+
justify-content: center;
|
|
1126
|
+
width: 1.5rem;
|
|
1127
|
+
height: 1.5rem;
|
|
1128
|
+
padding: 0;
|
|
1129
|
+
border: none;
|
|
1130
|
+
border-radius: 0.25rem;
|
|
1131
|
+
background: transparent;
|
|
1132
|
+
color: var(--ui-text-tertiary);
|
|
1133
|
+
cursor: pointer;
|
|
1134
|
+
transition: all 0.15s ease;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.input-clear-button:hover,
|
|
1138
|
+
.input-password-toggle:hover {
|
|
1139
|
+
background: var(--ui-bg-tertiary);
|
|
1140
|
+
color: var(--ui-text-primary);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.input-clear-button:focus-visible,
|
|
1144
|
+
.input-password-toggle:focus-visible {
|
|
1145
|
+
outline: none;
|
|
1146
|
+
box-shadow: 0 0 0 2px rgb(var(--ui-color-primary) / 0.5);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
:global([data-theme='dark']) .input-clear-button:focus-visible,
|
|
1150
|
+
:global([data-theme='dark']) .input-password-toggle:focus-visible {
|
|
1151
|
+
box-shadow: 0 0 0 2px rgb(var(--ui-color-primary-light) / 0.5);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
@media (prefers-color-scheme: dark) {
|
|
1155
|
+
:global(:root:not([data-theme='light'])) .input-clear-button:focus-visible,
|
|
1156
|
+
:global(:root:not([data-theme='light'])) .input-password-toggle:focus-visible {
|
|
1157
|
+
box-shadow: 0 0 0 2px rgb(var(--ui-color-primary-light) / 0.5);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.input-sm .input-clear-button,
|
|
1162
|
+
.input-sm .input-password-toggle {
|
|
1163
|
+
width: 1.25rem;
|
|
1164
|
+
height: 1.25rem;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
532
1167
|
/* ========================================
|
|
533
1168
|
FOOTER & MESSAGES
|
|
534
1169
|
======================================== */
|
|
@@ -548,6 +1183,12 @@
|
|
|
548
1183
|
animation: slide-in 0.2s ease-out;
|
|
549
1184
|
}
|
|
550
1185
|
|
|
1186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1187
|
+
.input-message {
|
|
1188
|
+
animation: none;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
551
1192
|
.input-message-error {
|
|
552
1193
|
color: rgb(var(--ui-color-error));
|
|
553
1194
|
}
|