-
- {#each selected as item (item.value)}
-
- {item.label}
-
-
- {/each}
+
+
+
+ {#if isOpen}
+
+
+
-
- {#if selected.length < max}
-
+
+
+ {#if filteredOptions.length > 0}
+ {#each filteredOptions as option, index (option.value)}
+ {#if customItems}
+
+ {:else}
+
selectOption(option)}
+ />
+ {/if}
+ {/each}
+ {:else}
+
+ No matches found
+
+ {/if}
+
+
{/if}
-
-
- {#if isOpen && filteredOptions.length > 0}
-
- {#each filteredOptions as option, index (option.value)}
-
- {/each}
-
- {/if}
-
-
- {#if isOpen && inputValue && filteredOptions.length === 0}
-
- No matches found
-
- {/if}
diff --git a/src/lib/client/ui/form/Input.svelte b/src/lib/client/ui/form/Input.svelte
index ed87769..8e83efb 100644
--- a/src/lib/client/ui/form/Input.svelte
+++ b/src/lib/client/ui/form/Input.svelte
@@ -9,6 +9,8 @@
export let compact: boolean = false;
// Responsive: auto-switch to compact on smaller screens (< 1280px)
export let responsive: boolean = false;
+ // Error state - shows red border
+ export let error: boolean = false;
const dispatch = createEventDispatcher<{ input: string }>();
@@ -51,7 +53,7 @@
{placeholder}
{disabled}
on:input={handleInput}
- class="{width} border border-neutral-300 bg-white text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500 {sizeClasses} {disabled
- ? 'cursor-not-allowed opacity-50'
- : ''}"
+ class="{width} border bg-white text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500 {sizeClasses} {error
+ ? 'border-red-400 bg-red-50 dark:border-red-500 dark:bg-red-900/20'
+ : 'border-neutral-300 dark:border-neutral-700'} {disabled ? 'cursor-not-allowed opacity-50' : ''}"
/>
diff --git a/src/lib/client/ui/form/NumberInput.svelte b/src/lib/client/ui/form/NumberInput.svelte
index 8bf442e..1c83aac 100644
--- a/src/lib/client/ui/form/NumberInput.svelte
+++ b/src/lib/client/ui/form/NumberInput.svelte
@@ -2,17 +2,18 @@
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { ChevronUp, ChevronDown } from 'lucide-svelte';
- const dispatch = createEventDispatcher<{ change: number }>();
+ const dispatch = createEventDispatcher<{ change: number | undefined }>();
// Props
export let name: string;
export let id: string = name;
- export let value: number;
+ export let value: number | undefined = undefined;
export let min: number | undefined = undefined;
export let max: number | undefined = undefined;
export let step: number = 1;
export let required: boolean = false;
export let disabled: boolean = false;
+ export let placeholder: string = '';
export let font: 'mono' | 'sans' | undefined = undefined;
export let compact: boolean = false;
// Responsive: auto-switch to compact on smaller screens (< 1280px)
@@ -58,28 +59,38 @@
// Increment/decrement handlers
function increment() {
- if (max !== undefined && value >= max) {
+ const currentValue = value ?? min ?? 0;
+ if (max !== undefined && currentValue >= max) {
onMaxBlocked?.();
return;
}
- updateValue(value + step);
+ updateValue(currentValue + step);
}
function decrement() {
- if (min !== undefined && value <= min) {
+ const currentValue = value ?? min ?? 0;
+ if (min !== undefined && currentValue <= min) {
onMinBlocked?.();
return;
}
- updateValue(value - step);
+ updateValue(currentValue - step);
}
// Validate on input
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
- let newValue = parseInt(target.value);
+
+ // Allow clearing the input (show placeholder)
+ if (target.value === '') {
+ value = undefined;
+ dispatch('change', undefined);
+ return;
+ }
+
+ let newValue = parseFloat(target.value);
if (isNaN(newValue)) {
- newValue = min ?? 0;
+ return; // Don't update for invalid input
}
if (min !== undefined && newValue < min) {
@@ -106,6 +117,7 @@
{step}
{required}
{disabled}
+ {placeholder}
class="block w-full [appearance:textfield] border border-neutral-300 bg-white text-neutral-900 placeholder-neutral-400 focus:outline-none disabled:bg-neutral-100 disabled:text-neutral-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:disabled:bg-neutral-900 dark:disabled:text-neutral-600 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none {inputSizeClasses} {fontClass}"
/>
diff --git a/src/lib/client/ui/form/Select.svelte b/src/lib/client/ui/form/Select.svelte
index 2072974..1fe5ddc 100644
--- a/src/lib/client/ui/form/Select.svelte
+++ b/src/lib/client/ui/form/Select.svelte
@@ -11,6 +11,7 @@
export let value: string = '';
export let placeholder: string = 'Select...';
export let mono: boolean = false;
+ export let width: string = 'w-full';
const dispatch = createEventDispatcher<{
change: string;
@@ -77,7 +78,7 @@
}
-
+