Skip to content

Commit 6ddc2a7

Browse files
committed
fix filters
1 parent b098b5d commit 6ddc2a7

File tree

1 file changed

+177
-52
lines changed

1 file changed

+177
-52
lines changed

src/Providers.svelte

Lines changed: 177 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
let filteredEndpoints: string[] = [];
1818
let searchQuery = "";
1919
let viewMode: "provider" | "endpoint" = "provider";
20+
let selectedFilter = "";
2021
let endpointColumns: string[] = [];
22+
let viewModeOpen = false;
23+
let filterOpen = false;
2124
2225
const PROVIDERS_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/provider_endpoints_support.json";
2326
const DOCS_URL = "https://docs.litellm.ai/docs/providers";
@@ -43,30 +46,59 @@
4346
console.error("Failed to load providers:", error);
4447
loading = false;
4548
}
49+
50+
// Close dropdowns when clicking outside
51+
const handleClickOutside = (event: MouseEvent) => {
52+
const target = event.target as HTMLElement;
53+
if (!target.closest('.custom-dropdown')) {
54+
viewModeOpen = false;
55+
filterOpen = false;
56+
}
57+
};
58+
document.addEventListener('click', handleClickOutside);
59+
60+
return () => {
61+
document.removeEventListener('click', handleClickOutside);
62+
};
4663
});
4764
65+
// Reset selected filter when view mode changes
66+
$: if (viewMode) {
67+
selectedFilter = "";
68+
}
69+
4870
$: {
49-
// Reset filters when view mode changes
50-
if (searchQuery.trim() === "") {
51-
filteredProviders = providers;
52-
filteredEndpoints = endpointColumns;
53-
} else {
71+
// Apply filters
72+
let tempProviders = providers;
73+
let tempEndpoints = endpointColumns;
74+
75+
// Apply search query
76+
if (searchQuery.trim() !== "") {
5477
const query = searchQuery.toLowerCase();
5578
if (viewMode === "provider") {
56-
filteredProviders = providers.filter((p) =>
79+
tempProviders = tempProviders.filter((p) =>
5780
p.provider.toLowerCase().includes(query) ||
5881
p.display_name.toLowerCase().includes(query)
5982
);
60-
filteredEndpoints = endpointColumns;
6183
} else {
62-
// In endpoint view, filter endpoints
63-
filteredProviders = providers;
64-
filteredEndpoints = endpointColumns.filter((endpoint) =>
84+
tempEndpoints = tempEndpoints.filter((endpoint) =>
6585
endpoint.toLowerCase().includes(query) ||
6686
formatEndpointName(endpoint).toLowerCase().includes(query)
6787
);
6888
}
6989
}
90+
91+
// Apply dropdown filter
92+
if (selectedFilter) {
93+
if (viewMode === "provider") {
94+
tempProviders = tempProviders.filter((p) => p.provider === selectedFilter);
95+
} else {
96+
tempEndpoints = tempEndpoints.filter((e) => e === selectedFilter);
97+
}
98+
}
99+
100+
filteredProviders = tempProviders;
101+
filteredEndpoints = tempEndpoints;
70102
}
71103
72104
// Initialize filtered endpoints when endpoint columns are ready
@@ -109,26 +141,9 @@
109141
</div>
110142
</div>
111143

112-
<!-- Search and View Mode -->
144+
<!-- Search and Filters -->
113145
<div class="search-section">
114-
<div class="search-bar-container">
115-
<div class="view-toggles">
116-
<button
117-
class="view-toggle"
118-
class:active={viewMode === "provider"}
119-
on:click={() => viewMode = "provider"}
120-
>
121-
View by Provider
122-
</button>
123-
<button
124-
class="view-toggle"
125-
class:active={viewMode === "endpoint"}
126-
on:click={() => viewMode = "endpoint"}
127-
>
128-
View by Endpoint
129-
</button>
130-
</div>
131-
146+
<div class="filters-row">
132147
<div class="search-input-wrapper">
133148
<svg class="search-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
134149
<circle cx="8.5" cy="8.5" r="5.75" stroke="currentColor" stroke-width="1.5"/>
@@ -142,6 +157,56 @@
142157
class="search-input"
143158
/>
144159
</div>
160+
161+
<!-- View Mode Dropdown -->
162+
<div class="custom-dropdown">
163+
<button class="dropdown-trigger" on:click|stopPropagation={() => viewModeOpen = !viewModeOpen} type="button">
164+
<span>{viewMode === "provider" ? "View by Provider" : "View by Endpoint"}</span>
165+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
166+
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
167+
</svg>
168+
</button>
169+
{#if viewModeOpen}
170+
<div class="dropdown-menu" transition:fly={{ y: -10, duration: 150 }}>
171+
<button class="dropdown-option" class:selected={viewMode === "provider"} on:click={() => { viewMode = "provider"; viewModeOpen = false; }} type="button">
172+
View by Provider
173+
</button>
174+
<button class="dropdown-option" class:selected={viewMode === "endpoint"} on:click={() => { viewMode = "endpoint"; viewModeOpen = false; }} type="button">
175+
View by Endpoint
176+
</button>
177+
</div>
178+
{/if}
179+
</div>
180+
181+
<!-- Filter Dropdown -->
182+
<div class="custom-dropdown">
183+
<button class="dropdown-trigger" on:click|stopPropagation={() => filterOpen = !filterOpen} type="button">
184+
<span>{selectedFilter ? (viewMode === "provider" ? providers.find(p => p.provider === selectedFilter)?.display_name.replace(/\s*\(.*?\)\s*$/, '') : formatEndpointName(selectedFilter)) : `All ${viewMode === "provider" ? "Providers" : "Endpoints"}`}</span>
185+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
186+
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
187+
</svg>
188+
</button>
189+
{#if filterOpen}
190+
<div class="dropdown-menu scrollable" transition:fly={{ y: -10, duration: 150 }}>
191+
<button class="dropdown-option" class:selected={!selectedFilter} on:click={() => { selectedFilter = ""; filterOpen = false; }} type="button">
192+
All {viewMode === "provider" ? "Providers" : "Endpoints"}
193+
</button>
194+
{#if viewMode === "provider"}
195+
{#each providers as { provider, display_name }}
196+
<button class="dropdown-option" class:selected={selectedFilter === provider} on:click={() => { selectedFilter = provider; filterOpen = false; }} type="button">
197+
{display_name.replace(/\s*\(.*?\)\s*$/, '')}
198+
</button>
199+
{/each}
200+
{:else}
201+
{#each endpointColumns as endpoint}
202+
<button class="dropdown-option" class:selected={selectedFilter === endpoint} on:click={() => { selectedFilter = endpoint; filterOpen = false; }} type="button">
203+
{formatEndpointName(endpoint)}
204+
</button>
205+
{/each}
206+
{/if}
207+
</div>
208+
{/if}
209+
</div>
145210
</div>
146211
</div>
147212

@@ -323,47 +388,107 @@
323388
padding: 0 2rem;
324389
}
325390
326-
.search-bar-container {
327-
display: flex;
391+
.filters-row {
392+
display: grid;
393+
grid-template-columns: 1fr auto auto;
328394
gap: 1rem;
329-
margin-bottom: 1rem;
330-
align-items: center;
395+
width: 100%;
396+
}
397+
398+
.custom-dropdown {
399+
position: relative;
400+
min-width: 220px;
331401
}
332402
333-
.view-toggles {
403+
.dropdown-trigger {
404+
width: 100%;
334405
display: flex;
335-
gap: 0.5rem;
336-
background: #f3f4f6;
337-
padding: 0.25rem;
406+
align-items: center;
407+
justify-content: space-between;
408+
gap: 0.75rem;
409+
padding: 0.875rem 1rem;
410+
font-size: 1rem;
411+
border: 1px solid #d1d5db;
338412
border-radius: 8px;
413+
background-color: #ffffff;
414+
cursor: pointer;
415+
transition: all 0.2s ease;
416+
font-family: inherit;
417+
color: var(--contrast);
418+
height: 48px;
419+
box-sizing: border-box;
339420
}
340421
341-
.view-toggle {
342-
padding: 0.625rem 1.25rem;
343-
border: none;
344-
background: transparent;
345-
color: var(--muted-color);
422+
.dropdown-trigger:hover {
423+
border-color: #9ca3af;
424+
}
425+
426+
.dropdown-trigger:focus {
427+
outline: none;
428+
border-color: #6366f1;
429+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
430+
}
431+
432+
.dropdown-trigger span {
433+
flex: 1;
434+
text-align: left;
435+
white-space: nowrap;
436+
overflow: hidden;
437+
text-overflow: ellipsis;
346438
font-weight: 500;
347-
font-size: 0.9375rem;
348-
border-radius: 6px;
439+
}
440+
441+
.dropdown-trigger svg {
442+
flex-shrink: 0;
443+
color: var(--muted-color);
444+
}
445+
446+
.dropdown-menu {
447+
position: absolute;
448+
top: calc(100% + 0.5rem);
449+
left: 0;
450+
right: 0;
451+
background-color: #ffffff;
452+
border: 1px solid #d1d5db;
453+
border-radius: 8px;
454+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
455+
z-index: 9999;
456+
overflow: hidden;
457+
}
458+
459+
.dropdown-menu.scrollable {
460+
max-height: 300px;
461+
overflow-y: auto;
462+
}
463+
464+
.dropdown-option {
465+
width: 100%;
466+
padding: 0.75rem 1rem;
467+
text-align: left;
468+
background: none;
469+
border: none;
349470
cursor: pointer;
350-
transition: all 0.2s ease;
471+
font-size: 0.9375rem;
472+
color: var(--contrast);
473+
transition: background-color 0.15s ease;
351474
white-space: nowrap;
475+
overflow: hidden;
476+
text-overflow: ellipsis;
352477
}
353478
354-
.view-toggle:hover {
355-
color: var(--contrast);
479+
.dropdown-option:hover {
480+
background-color: #f3f4f6;
356481
}
357482
358-
.view-toggle.active {
359-
background: #ffffff;
360-
color: var(--contrast);
361-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
483+
.dropdown-option.selected {
484+
background-color: #eff6ff;
485+
color: #2563eb;
486+
font-weight: 500;
362487
}
363488
364489
.search-input-wrapper {
365490
position: relative;
366-
flex: 1;
491+
width: 100%;
367492
}
368493
369494
.search-icon {

0 commit comments

Comments
 (0)