diff --git a/src/lib/client/stores/search.ts b/src/lib/client/stores/search.ts new file mode 100644 index 0000000..7af2cad --- /dev/null +++ b/src/lib/client/stores/search.ts @@ -0,0 +1,141 @@ +/** + * Search store for managing search and filter state + */ + +import { writable, derived, get } from 'svelte/store'; + +export interface SearchState { + query: string; + filters: Record; + isActive: boolean; +} + +export interface SearchStoreConfig { + debounceMs?: number; + caseSensitive?: boolean; +} + +export function createSearchStore(config: SearchStoreConfig = {}) { + const { debounceMs = 300, caseSensitive = false } = config; + + const state = writable({ + query: '', + filters: {}, + isActive: false + }); + + let debounceTimer: ReturnType | null = null; + + // Derived store for the debounced query + const debouncedQuery = writable(''); + + function setQuery(query: string) { + state.update((s) => ({ ...s, query, isActive: query.length > 0 })); + + // Debounce the query update + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + debounceTimer = setTimeout(() => { + debouncedQuery.set(query); + }, debounceMs); + } + + function setFilter(key: string, value: any) { + state.update((s) => ({ + ...s, + filters: { ...s.filters, [key]: value }, + isActive: true + })); + } + + function removeFilter(key: string) { + state.update((s) => { + const { [key]: _, ...rest } = s.filters; + return { + ...s, + filters: rest, + isActive: s.query.length > 0 || Object.keys(rest).length > 0 + }; + }); + } + + function clearFilters() { + state.update((s) => ({ + ...s, + filters: {}, + isActive: s.query.length > 0 + })); + } + + function clear() { + state.set({ + query: '', + filters: {}, + isActive: false + }); + debouncedQuery.set(''); + if (debounceTimer) { + clearTimeout(debounceTimer); + } + } + + // Helper function to filter an array of items + function filterItems( + items: T[], + searchFields: (keyof T)[], + additionalFilter?: (item: T, filters: Record) => boolean + ): T[] { + const currentState = get(state); + const query = get(debouncedQuery); + + if (!currentState.isActive) { + return items; + } + + return items.filter((item) => { + // Search query filter + if (query) { + const matchesQuery = searchFields.some((field) => { + const value = String(item[field] ?? ''); + return caseSensitive + ? value.includes(query) + : value.toLowerCase().includes(query.toLowerCase()); + }); + + if (!matchesQuery) { + return false; + } + } + + // Additional custom filters + if (additionalFilter && Object.keys(currentState.filters).length > 0) { + return additionalFilter(item, currentState.filters); + } + + return true; + }); + } + + // Derived store for easy access to whether search is active + const isActive = derived(state, ($state) => $state.isActive); + + // Derived store for filter count + const filterCount = derived(state, ($state) => Object.keys($state.filters).length); + + return { + subscribe: state.subscribe, + debouncedQuery: { subscribe: debouncedQuery.subscribe }, + isActive: { subscribe: isActive.subscribe }, + filterCount: { subscribe: filterCount.subscribe }, + setQuery, + setFilter, + removeFilter, + clearFilters, + clear, + filterItems + }; +} + +export type SearchStore = ReturnType; diff --git a/src/lib/client/ui/actions/ActionButton.svelte b/src/lib/client/ui/actions/ActionButton.svelte new file mode 100644 index 0000000..dad4f9c --- /dev/null +++ b/src/lib/client/ui/actions/ActionButton.svelte @@ -0,0 +1,52 @@ + + +
+ + + {#if hasDropdown && isHovered} +
+ +
+ {/if} +
diff --git a/src/lib/client/ui/actions/ActionsBar.svelte b/src/lib/client/ui/actions/ActionsBar.svelte new file mode 100644 index 0000000..bbc37fc --- /dev/null +++ b/src/lib/client/ui/actions/ActionsBar.svelte @@ -0,0 +1,28 @@ + + +
+ +
+ + diff --git a/src/lib/client/ui/actions/SearchAction.svelte b/src/lib/client/ui/actions/SearchAction.svelte new file mode 100644 index 0000000..07d1aaa --- /dev/null +++ b/src/lib/client/ui/actions/SearchAction.svelte @@ -0,0 +1,56 @@ + + +
+
+ +
+ +
+ + + (isFocused = true)} + on:blur={() => (isFocused = false)} + {placeholder} + class="h-full w-full bg-transparent pl-10 pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400" + /> + + + {#if query} + + {/if} +
+
diff --git a/src/lib/client/ui/dropdown/Dropdown.svelte b/src/lib/client/ui/dropdown/Dropdown.svelte new file mode 100644 index 0000000..7b682c2 --- /dev/null +++ b/src/lib/client/ui/dropdown/Dropdown.svelte @@ -0,0 +1,23 @@ + + + + +
+ +
+ +
diff --git a/src/lib/client/ui/dropdown/DropdownItem.svelte b/src/lib/client/ui/dropdown/DropdownItem.svelte new file mode 100644 index 0000000..390eaa4 --- /dev/null +++ b/src/lib/client/ui/dropdown/DropdownItem.svelte @@ -0,0 +1,29 @@ + + +