feat: replace hardcoded protocol filters with searchable 217-language dropdown

Author: Aaron Steven White
Commit 38eb4efa08ade58bdd49ca5c05f17cf20e53d567
Parent: 69ab7395b1
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
1 file changed +78 -31
@@ -1,20 +1,38 @@
11 <script lang="ts">
22 	import RepoCard from '$lib/components/repo/RepoCard.svelte';
3+	import { ALL_LANGUAGES } from '$lib/data/languages';
4+	import { goto } from '$app/navigation';
35 
46 	let { data } = $props();
57 
6-	const protocols: { value: string; label: string; color: string }[] = [
7-		{ value: 'typescript', label: 'TypeScript', color: 'oklch(0.65 0.15 230)' },
8-		{ value: 'python', label: 'Python', color: 'oklch(0.65 0.15 90)' },
9-		{ value: 'rust', label: 'Rust', color: 'oklch(0.60 0.18 30)' },
10-		{ value: 'go', label: 'Go', color: 'oklch(0.65 0.15 195)' },
11-		{ value: 'protobuf', label: 'Protobuf', color: 'oklch(0.65 0.12 145)' },
12-		{ value: 'graphql', label: 'GraphQL', color: 'oklch(0.55 0.20 330)' },
13-		{ value: 'sql', label: 'SQL', color: 'oklch(0.65 0.10 260)' },
14-		{ value: 'atproto_lexicon', label: 'ATProto', color: 'oklch(0.65 0.15 250)' },
15-	];
16-
178 	let activeProtocol = $derived(data.protocol);
9+	let searchQuery = $state('');
10+	let showDropdown = $state(false);
11+
12+	let filtered = $derived(
13+		searchQuery.trim()
14+			? ALL_LANGUAGES.filter((l) =>
15+					l.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
16+					l.value.toLowerCase().includes(searchQuery.toLowerCase())
17+				).slice(0, 20)
18+			: []
19+	);
20+
21+	function selectProtocol(value: string) {
22+		showDropdown = false;
23+		searchQuery = '';
24+		goto(`/?protocol=${value}`);
25+	}
26+
27+	// Generate a deterministic color from language name
28+	function langColor(value: string): string {
29+		let hash = 0;
30+		for (let i = 0; i < value.length; i++) {
31+			hash = value.charCodeAt(i) + ((hash << 5) - hash);
32+		}
33+		const hue = Math.abs(hash % 360);
34+		return `oklch(0.65 0.15 ${hue})`;
35+	}
1836 </script>
1937 
2038 <svelte:head>
@@ -27,32 +45,61 @@
2745 		Discover repositories across protocols on the AT Protocol network.
2846 	</p>
2947 
30-	<!-- Protocol filters -->
31-	<div class="mb-8 flex items-center gap-2 overflow-x-auto pb-2 scrollbar-none">
32-		<a
33-			href="/"
34-			class="shrink-0 whitespace-nowrap rounded-full border px-3 py-1 text-xs font-medium transition-colors
35-				{!activeProtocol
36-					? 'border-accent bg-accent/10 text-accent'
37-					: 'border-border text-text-secondary hover:border-accent/30 hover:text-text-primary'}"
38-		>
39-			All
40-		</a>
41-		{#each protocols as proto (proto.value)}
48+	<!-- Language filter -->
49+	<div class="mb-8">
50+		<div class="flex flex-wrap items-center gap-2">
4251 			<a
43-				href="/?protocol={proto.value}"
52+				href="/"
4453 				class="shrink-0 whitespace-nowrap rounded-full border px-3 py-1 text-xs font-medium transition-colors
45-					{activeProtocol === proto.value
54+					{!activeProtocol
4655 						? 'border-accent bg-accent/10 text-accent'
4756 						: 'border-border text-text-secondary hover:border-accent/30 hover:text-text-primary'}"
4857 			>
49-				<span
50-					class="mr-1.5 inline-block h-2 w-2 rounded-full"
51-					style="background-color: {proto.color}"
52-				></span>
53-				{proto.label}
58+				All {ALL_LANGUAGES.length} languages
5459 			</a>
55-		{/each}
60+
61+			{#if activeProtocol}
62+				<span class="flex items-center gap-1.5 rounded-full border border-accent bg-accent/10 px-3 py-1 text-xs font-medium text-accent">
63+					<span class="h-2 w-2 rounded-full" style="background-color: {langColor(activeProtocol)}"></span>
64+					{ALL_LANGUAGES.find((l) => l.value === activeProtocol)?.label ?? activeProtocol}
65+					<a href="/" class="ml-1 hover:text-text-primary">
66+						<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
67+							<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
68+						</svg>
69+					</a>
70+				</span>
71+			{/if}
72+
73+			<!-- Search input -->
74+			<div class="relative">
75+				<input
76+					type="text"
77+					bind:value={searchQuery}
78+					onfocus={() => showDropdown = true}
79+					onblur={() => setTimeout(() => showDropdown = false, 200)}
80+					placeholder="Filter by language..."
81+					class="w-44 rounded-full border border-border bg-surface-0 px-3 py-1 text-xs text-text-primary placeholder:text-text-secondary focus:border-accent focus:outline-none"
82+				/>
83+
84+				{#if showDropdown && filtered.length > 0}
85+					<ul class="absolute left-0 top-full z-50 mt-1 max-h-48 w-56 overflow-y-auto rounded-lg border border-border bg-surface-1 shadow-lg">
86+						{#each filtered as lang}
87+							<li>
88+								<button
89+									type="button"
90+									onmousedown={() => selectProtocol(lang.value)}
91+									class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-surface-2
92+										{activeProtocol === lang.value ? 'text-accent' : 'text-text-secondary'}"
93+								>
94+									<span class="h-2 w-2 shrink-0 rounded-full" style="background-color: {langColor(lang.value)}"></span>
95+									{lang.label}
96+								</button>
97+							</li>
98+						{/each}
99+					</ul>
100+				{/if}
101+			</div>
102+		</div>
56103 	</div>
57104 
58105 	<!-- Trending section -->
cospan · schematic version control on atproto built on AT Protocol