+
{@render children()}
{:else}
@@ -89,24 +115,37 @@
font-size: typography.$font-regular;
min-height: calc(spacing.$unit * 5);
- &:hover:not(.editable) {
+ &:hover:not(.editable):not(.hasChildren) {
background: colors.$grey-80;
}
- &.editable:hover,
- &.editable:focus-within {
+ &.editable:focus-within,
+ &.hasChildren:focus-within {
background: var(--input-bg-hover);
}
- &.editable {
+ &.editable,
+ &.hasChildren {
background: var(--input-bg);
}
+ .label-container {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ margin-right: spacing.$unit-2x;
+ gap: spacing.$unit-fourth;
+ }
+
.label {
font-weight: typography.$medium;
color: colors.$grey-50;
- flex-shrink: 0;
- margin-right: spacing.$unit-2x;
+ }
+
+ .sublabel {
+ font-size: typography.$font-small;
+ color: colors.$grey-60;
+ font-weight: typography.$normal;
}
.value {
@@ -123,13 +162,12 @@
:global(.input),
:global(.select) {
- min-width: 180px;
+ width: var(--custom-width, 240px);
}
:global(.input.number) {
- min-width: 120px;
+ width: var(--custom-width, 120px);
}
}
-
}
diff --git a/src/lib/components/ui/Input.svelte b/src/lib/components/ui/Input.svelte
index 37d55703..18617c3e 100644
--- a/src/lib/components/ui/Input.svelte
+++ b/src/lib/components/ui/Input.svelte
@@ -18,6 +18,10 @@
alignRight?: boolean
accessory?: boolean
no1password?: boolean
+ validationState?: 'idle' | 'validating' | 'valid' | 'invalid'
+ handleBlur?: () => void
+ handleFocus?: () => void
+ handleInput?: () => void
}
let {
@@ -34,7 +38,7 @@
fullHeight = false,
alignRight = false,
accessory = false,
- value = $bindable(''),
+ value = $bindable(),
type = 'text',
placeholder,
disabled = false,
@@ -42,12 +46,27 @@
required = false,
class: className = '',
no1password = false,
+ validationState = 'idle',
+ handleBlur,
+ handleFocus,
+ handleInput,
...restProps
}: Props = $props()
+ // Determine the validation icon to show
+ const validationIcon = $derived(
+ validationState === 'validating'
+ ? 'loader-2'
+ : validationState === 'valid'
+ ? 'check'
+ : validationState === 'invalid'
+ ? 'close'
+ : null
+ )
+
const showCounter = $derived(counter !== undefined || maxLength !== undefined)
- const currentCount = $derived(String(value).length)
- const hasWrapper = $derived(accessory || leftIcon || rightIcon || showCounter)
+ const currentCount = $derived(String(value ?? '').length)
+ const hasWrapper = $derived(accessory || leftIcon || rightIcon || showCounter || validationIcon)
const fieldsetClasses = $derived(
['fieldset', hidden && 'hidden', fullWidth && 'full', className].filter(Boolean).join(' ')
@@ -68,6 +87,12 @@
.filter(Boolean)
.join(' ')
)
+
+ // Debug: log what's in restProps
+ $effect(() => {
+ console.log('[Input] restProps keys:', Object.keys(restProps))
+ console.log('[Input] hasWrapper:', hasWrapper, 'validationIcon:', validationIcon)
+ })
{#if label || error}
@@ -98,6 +123,9 @@
{required}
maxlength={maxLength}
data-1p-ignore={no1password}
+ onblur={handleBlur}
+ onfocus={handleFocus}
+ oninput={handleInput}
{...restProps}
/>
@@ -107,6 +135,12 @@
{/if}
+ {#if validationIcon}
+
+
+
+ {/if}
+
{#if showCounter}
{currentCount}{maxLength ? `/${maxLength}` : ''}
@@ -124,6 +158,9 @@
{required}
maxlength={maxLength}
data-1p-ignore={no1password}
+ onblur={handleBlur}
+ onfocus={handleFocus}
+ oninput={handleInput}
{...restProps}
/>
{/if}
@@ -149,6 +186,9 @@
{required}
maxlength={maxLength}
data-1p-ignore={no1password}
+ onblur={handleBlur}
+ onfocus={handleFocus}
+ oninput={handleInput}
{...restProps}
/>
@@ -158,6 +198,12 @@
{/if}
+ {#if validationIcon}
+
+
+
+ {/if}
+
{#if showCounter}
{currentCount}{maxLength ? `/${maxLength}` : ''}
@@ -175,6 +221,9 @@
{required}
maxlength={maxLength}
data-1p-ignore={no1password}
+ onblur={handleBlur}
+ onfocus={handleFocus}
+ oninput={handleInput}
{...restProps}
/>
{/if}
@@ -312,6 +361,34 @@
right: $unit-2x;
}
+ .validationIcon {
+ position: absolute;
+ right: $unit-2x;
+ display: flex;
+ align-items: center;
+ pointer-events: none;
+
+ :global(svg) {
+ fill: currentColor;
+ }
+
+ &.valid {
+ color: $wind-text-20;
+ }
+
+ &.invalid {
+ color: $error;
+ }
+
+ &.validating {
+ color: var(--text-tertiary);
+
+ :global(svg) {
+ animation: spin 1s linear infinite;
+ }
+ }
+ }
+
&:has(.iconLeft) input {
padding-left: $unit-5x;
}
@@ -320,6 +397,10 @@
padding-right: $unit-5x;
}
+ &:has(.validationIcon) input {
+ padding-right: $unit-5x;
+ }
+
&:has(.counter) input {
padding-right: $unit-8x;
}
@@ -479,4 +560,13 @@
color: var(--text-tertiary);
opacity: 1;
}
+
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
diff --git a/src/lib/components/ui/ValidatedInput.svelte b/src/lib/components/ui/ValidatedInput.svelte
new file mode 100644
index 00000000..33d974f0
--- /dev/null
+++ b/src/lib/components/ui/ValidatedInput.svelte
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/ui/checkbox/Checkbox.svelte b/src/lib/components/ui/checkbox/Checkbox.svelte
index 4d0e984a..197e1080 100644
--- a/src/lib/components/ui/checkbox/Checkbox.svelte
+++ b/src/lib/components/ui/checkbox/Checkbox.svelte
@@ -40,12 +40,6 @@
element
}: Props = $props()
- $effect(() => {
- if (onCheckedChange && checked !== undefined) {
- onCheckedChange(checked)
- }
- })
-
const sizeClass = $derived(size)
// contained prop is an alias for variant='bound'
const variantClass = $derived(variant === 'bound' || contained ? 'bound' : '')
@@ -57,6 +51,7 @@
{indeterminate}
{disabled}
{required}
+ {onCheckedChange}
name={name ?? ''}
value={value ?? ''}
class="checkbox {sizeClass} {variantClass} {fullWidthClass} {element || ''} {className || ''}"