From 97c21b95729745e2f5717ab00075144fc2d5ea0a Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Thu, 22 Jan 2026 15:17:18 +1030 Subject: [PATCH] feat: condition improvements - refactor cards into unified component with modes - add placeholders to dropdown selects - style autocomplete similar to other ui components - add placeholders to number inputs - show any in language conditions - add boolean for except langauge --- src/lib/client/ui/button/Button.svelte | 7 +- .../client/ui/dropdown/DropdownSelect.svelte | 11 +- src/lib/client/ui/form/Autocomplete.svelte | 212 +++++---- src/lib/client/ui/form/Input.svelte | 8 +- src/lib/client/ui/form/NumberInput.svelte | 28 +- src/lib/client/ui/form/Select.svelte | 3 +- src/lib/server/sync/mappings.ts | 7 +- .../[id]/conditions/+page.server.ts | 12 +- .../[databaseId]/[id]/conditions/+page.svelte | 17 +- .../components/ConditionCard.svelte | 365 +++++++++------- .../components/DraftConditionCard.svelte | 404 ------------------ 11 files changed, 384 insertions(+), 690 deletions(-) delete mode 100644 src/routes/custom-formats/[databaseId]/[id]/conditions/components/DraftConditionCard.svelte diff --git a/src/lib/client/ui/button/Button.svelte b/src/lib/client/ui/button/Button.svelte index 7b5ba9f..84e170f 100644 --- a/src/lib/client/ui/button/Button.svelte +++ b/src/lib/client/ui/button/Button.svelte @@ -16,6 +16,8 @@ export let fullWidth: boolean = false; // Optional href - renders as anchor instead of button export let href: string | undefined = undefined; + // Alignment for content (center or between for dropdowns) + export let justify: 'center' | 'between' = 'center'; let isSmallScreen = false; let mediaQuery: MediaQueryList | null = null; @@ -38,8 +40,9 @@ isSmallScreen = e.matches; } - const baseClasses = - 'inline-flex items-center justify-center font-medium transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50'; + $: justifyClass = justify === 'between' ? 'justify-between' : 'justify-center'; + + $: baseClasses = `inline-flex items-center ${justifyClass} font-medium transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50`; const sizeClasses = { xs: 'gap-1 rounded px-2 py-1 text-xs', diff --git a/src/lib/client/ui/dropdown/DropdownSelect.svelte b/src/lib/client/ui/dropdown/DropdownSelect.svelte index 2a8c34f..de4c263 100644 --- a/src/lib/client/ui/dropdown/DropdownSelect.svelte +++ b/src/lib/client/ui/dropdown/DropdownSelect.svelte @@ -9,6 +9,7 @@ export let label: string | undefined = undefined; export let value: string; export let options: { value: string; label: string; description?: string }[]; + export let placeholder: string = 'Select...'; export let minWidth: string = '8rem'; export let position: 'left' | 'right' | 'middle' = 'left'; // Separate compact controls - compact is shorthand for both @@ -22,6 +23,8 @@ export let fullWidth: boolean = false; // Fixed positioning to escape overflow containers (e.g. tables) export let fixed: boolean = false; + // Custom width class (overrides fullWidth if set) + export let width: string | undefined = undefined; const dispatch = createEventDispatcher<{ change: string }>(); @@ -48,7 +51,9 @@ isSmallScreen = e.matches; } - $: currentLabel = options.find((o) => o.value === value)?.label ?? value; + $: matchedOption = options.find((o) => o.value === value); + $: currentLabel = matchedOption?.label || placeholder; + $: isPlaceholder = !matchedOption; $: isCompactButton = compactButton ?? (responsiveButton ? isSmallScreen : compact); $: isCompactDropdown = compactDropdown !== undefined @@ -67,7 +72,7 @@ } -
+
{#if label} {label} {/if} @@ -83,6 +88,8 @@ iconPosition="right" size={buttonSize} {fullWidth} + justify={fullWidth || width ? 'between' : 'center'} + textColor={isPlaceholder ? 'text-neutral-400 dark:text-neutral-500' : ''} on:click={() => (open = !open)} /> {#if open} diff --git a/src/lib/client/ui/form/Autocomplete.svelte b/src/lib/client/ui/form/Autocomplete.svelte index 4719858..9ca4914 100644 --- a/src/lib/client/ui/form/Autocomplete.svelte +++ b/src/lib/client/ui/form/Autocomplete.svelte @@ -1,31 +1,40 @@ -
- -
- - {#each selected as item (item.value)} -
- {item.label} - -
- {/each} +
+
+ + {: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 @@ } -
+