|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount } from "svelte"; |
| 3 | + import { fade } from "svelte/transition"; |
3 | 4 | import App from "./App.svelte"; |
4 | 5 | import Providers from "./Providers.svelte"; |
5 | 6 | import Cookbook from "./Cookbook.svelte"; |
|
44 | 45 | activeTab = tab; |
45 | 46 | closeMobileMenu(); |
46 | 47 | trackTabChange(tab); |
| 48 | + window.scrollTo({ top: 0, behavior: "smooth" }); |
47 | 49 | |
48 | 50 | if (updateUrl) { |
49 | 51 | const path = getPathFromTab(tab); |
|
64 | 66 | let modelCount = 0; |
65 | 67 | let statsLoading = true; |
66 | 68 |
|
| 69 | + let displayModelCount = 0; |
| 70 | + let displayProviderCount = 0; |
| 71 | + let displayEndpointCount = 0; |
| 72 | + let displayComboCount = 0; |
| 73 | +
|
| 74 | + function animateValue(start: number, end: number, duration: number, setter: (v: number) => void) { |
| 75 | + const startTime = performance.now(); |
| 76 | + function step(now: number) { |
| 77 | + const elapsed = now - startTime; |
| 78 | + const progress = Math.min(elapsed / duration, 1); |
| 79 | + const eased = 1 - Math.pow(1 - progress, 3); |
| 80 | + setter(Math.round(start + (end - start) * eased)); |
| 81 | + if (progress < 1) requestAnimationFrame(step); |
| 82 | + } |
| 83 | + requestAnimationFrame(step); |
| 84 | + } |
| 85 | +
|
67 | 86 | onMount(async () => { |
68 | 87 | initAnalytics(); |
69 | 88 | trackPageView('Home'); |
|
109 | 128 | modelCount = Object.keys(modelsData).length; |
110 | 129 | |
111 | 130 | statsLoading = false; |
| 131 | +
|
| 132 | + animateValue(0, modelCount, 800, (v) => displayModelCount = v); |
| 133 | + animateValue(0, providerCount, 600, (v) => displayProviderCount = v); |
| 134 | + animateValue(0, endpointCount, 600, (v) => displayEndpointCount = v); |
| 135 | + animateValue(0, providerEndpointCount, 700, (v) => displayComboCount = v); |
112 | 136 | } catch (error) { |
113 | 137 | console.error("Failed to load statistics:", error); |
114 | 138 | statsLoading = false; |
|
127 | 151 | <!-- Header --> |
128 | 152 | <header class="header"> |
129 | 153 | <div class="header-content"> |
130 | | - <div class="logo-section-header"> |
| 154 | + <a href="/" class="logo-section-header" on:click|preventDefault={() => selectTab("models")}> |
131 | 155 | <span class="logo-emoji">🚅</span> |
132 | 156 | <span class="logo-text-header">LiteLLM</span> |
133 | | - </div> |
| 157 | + </a> |
134 | 158 |
|
135 | 159 | <!-- Desktop Navigation --> |
136 | 160 | <div class="desktop-nav"> |
|
256 | 280 | <div class="stats-section"> |
257 | 281 | <div class="stats-container"> |
258 | 282 | <button class="stat-card" on:click={() => selectTab("models")}> |
259 | | - <div class="stat-value">{modelCount.toLocaleString()}</div> |
| 283 | + <div class="stat-value">{displayModelCount.toLocaleString()}</div> |
260 | 284 | <div class="stat-label">Models Supported</div> |
261 | 285 | </button> |
262 | 286 | <button class="stat-card" on:click={() => selectTab("providers")}> |
263 | | - <div class="stat-value">{providerCount}</div> |
| 287 | + <div class="stat-value">{displayProviderCount.toLocaleString()}</div> |
264 | 288 | <div class="stat-label">Providers</div> |
265 | 289 | </button> |
266 | 290 | <button class="stat-card" on:click={() => selectTab("providers")}> |
267 | | - <div class="stat-value">{endpointCount}</div> |
| 291 | + <div class="stat-value">{displayEndpointCount.toLocaleString()}</div> |
268 | 292 | <div class="stat-label">Unique Endpoints</div> |
269 | 293 | </button> |
270 | 294 | <button class="stat-card" on:click={() => selectTab("providers")}> |
271 | | - <div class="stat-value">{providerEndpointCount}</div> |
| 295 | + <div class="stat-value">{displayComboCount.toLocaleString()}</div> |
272 | 296 | <div class="stat-label">Provider + Endpoint Combos</div> |
273 | 297 | </button> |
274 | 298 | </div> |
275 | 299 | </div> |
276 | 300 | {/if} |
277 | 301 |
|
278 | 302 | <!-- Content --> |
279 | | - {#if activeTab === "models"} |
280 | | - <App /> |
281 | | - {:else if activeTab === "providers"} |
282 | | - <Providers /> |
283 | | - {:else if activeTab === "cookbook"} |
284 | | - <Cookbook /> |
285 | | - {:else if activeTab === "guardrails"} |
286 | | - <Guardrails /> |
287 | | - {/if} |
| 303 | + {#key activeTab} |
| 304 | + <div in:fade={{ duration: 150, delay: 50 }}> |
| 305 | + {#if activeTab === "models"} |
| 306 | + <App /> |
| 307 | + {:else if activeTab === "providers"} |
| 308 | + <Providers /> |
| 309 | + {:else if activeTab === "cookbook"} |
| 310 | + <Cookbook /> |
| 311 | + {:else if activeTab === "guardrails"} |
| 312 | + <Guardrails /> |
| 313 | + {/if} |
| 314 | + </div> |
| 315 | + {/key} |
288 | 316 |
|
289 | 317 | <!-- Request Form Modal --> |
290 | 318 | <RequestForm bind:this={requestForm} /> |
|
301 | 329 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
302 | 330 | } |
303 | 331 |
|
| 332 | + :global(::selection) { |
| 333 | + background: rgba(99, 102, 241, 0.2); |
| 334 | + color: inherit; |
| 335 | + } |
| 336 | +
|
| 337 | + :global(*:focus-visible) { |
| 338 | + outline: 2px solid var(--litellm-primary); |
| 339 | + outline-offset: 2px; |
| 340 | + } |
| 341 | +
|
| 342 | + :global(input:focus-visible), |
| 343 | + :global(textarea:focus-visible), |
| 344 | + :global(select:focus-visible) { |
| 345 | + outline: none; |
| 346 | + } |
| 347 | +
|
304 | 348 | :global(*::-webkit-scrollbar) { |
305 | 349 | width: 8px; |
306 | 350 | height: 8px; |
|
420 | 464 | display: flex; |
421 | 465 | align-items: center; |
422 | 466 | gap: 0.5rem; |
| 467 | + text-decoration: none; |
| 468 | + color: inherit; |
423 | 469 | } |
424 | 470 |
|
425 | 471 | .logo-emoji { |
|
0 commit comments