fix: Tangled morphisms — use renamed_morphism + panproto type coercions Root cause: all cross-NSID morphisms used identity_morphism which maps vertices with the SAME ID. But sh.tangled.* and dev.cospan.* have different NSID-prefixed vertex IDs, so the identity morphism mapped NOTHING — causing Restrict(RootPruned) at runtime. Fix: replace all identity_morphism calls with renamed_morphism which explicitly maps the NSID-prefixed vertices across namespaces. Add Tangled-specific FieldTransforms using panproto expressions: - actor.profile: ApplyExpr with Match to coerce boolean bluesky → string - repo: ComputeField with Concat to build did:web:{knot} and https://{knot} - knot: ComputeField with Concat for publicEndpoint from hostname - knot.member: RenameField knot→orgUri, subject→memberDid - spindle: ComputeField hostname→name - spindle.member: RenameField spindle→orgUri, subject→memberDid All type coercions use panproto's expression language — no string munging.
Author: Aaron Steven White
Commit
300050bd2d782ae5cf1572bbdad3c94a91abbaa1Parent: 03025bba03
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-cospan7 files changed +567 -281
@@ -1,70 +1,112 @@
11 @import "tailwindcss"; 2+@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Mono:wght@300;400;500&display=swap'); 23 34 @theme { 4- /* Base: blue-tinted dark, mathematical precision */ 5- --color-bg: oklch(0.11 0.008 270); 6- --color-surface-0: oklch(0.14 0.008 270); 7- --color-surface-1: oklch(0.17 0.008 270); 8- --color-surface-2: oklch(0.21 0.01 270); 9- --color-border: oklch(0.24 0.008 270); 10- --color-border-hover: oklch(0.35 0.01 270); 11- --color-text-primary: oklch(0.90 0.01 270); 12- --color-text-secondary: oklch(0.58 0.01 270); 13- --color-text-muted: oklch(0.40 0.01 270); 14- --color-accent: oklch(0.65 0.14 250); 15- --color-accent-hover: oklch(0.72 0.14 250); 5+ /* ── Palette: deep indigo-black with cold precision ── */ 6+ --color-void: #07080f; 7+ --color-ground: #0c0d17; 8+ --color-surface: #12131f; 9+ --color-raised: #1a1b2a; 10+ --color-elevated: #222336; 11+ --color-line: #2a2b40; 12+ --color-line-bright: #3d3e5a; 13+ 14+ /* Text: high contrast hierarchy */ 15+ --color-ink: #e8e9f0; 16+ --color-caption: #8b8da3; 17+ --color-ghost: #4e5068; 18+ 19+ /* Accent: a single electric blue, used sparingly */ 20+ --color-focus: #5b7ff5; 21+ --color-focus-bright: #7b9aff; 22+ --color-focus-dim: #3a5ad1; 1623 1724 /* Semantic */ 18- --color-success: oklch(0.60 0.14 155); 19- --color-danger: oklch(0.60 0.14 25); 20- --color-warning: oklch(0.65 0.14 85); 21- --color-info: oklch(0.60 0.10 280); 25+ --color-ok: #3dd68c; 26+ --color-err: #f55b6a; 27+ --color-warn: #f5c542; 28+ --color-info: #5bb8f5; 29+ 30+ /* Legacy aliases */ 31+ --color-breaking: #f55b6a; 32+ --color-compatible: #3dd68c; 33+ --color-conflict: #f5c542; 34+ --color-lens: #5bb8f5; 35+ --color-accent: #5b7ff5; 36+ --color-accent-hover: #7b9aff; 37+ --color-success: #3dd68c; 38+ --color-danger: #f55b6a; 39+ --color-warning: #f5c542; 2240 23- /* Legacy aliases for existing components */ 24- --color-breaking: oklch(0.60 0.14 25); 25- --color-compatible: oklch(0.60 0.14 155); 26- --color-conflict: oklch(0.65 0.14 85); 27- --color-lens: oklch(0.60 0.10 280); 41+ /* Backwards compat tokens */ 42+ --color-bg: #07080f; 43+ --color-surface-0: #0c0d17; 44+ --color-surface-1: #12131f; 45+ --color-surface-2: #1a1b2a; 46+ --color-border: #2a2b40; 47+ --color-border-hover: #3d3e5a; 48+ --color-text-primary: #e8e9f0; 49+ --color-text-secondary: #8b8da3; 50+ --color-text-muted: #4e5068; 2851 2952 /* Typography */ 30- --font-sans: 'Inter Variable', 'Inter', system-ui, sans-serif; 31- --font-mono: 'JetBrains Mono', 'Fira Code', monospace; 53+ --font-sans: 'DM Sans', system-ui, -apple-system, sans-serif; 54+ --font-mono: 'DM Mono', 'JetBrains Mono', 'SF Mono', monospace; 55+} 56+ 57+/* ── Base ── */ 58+html { 59+ -webkit-font-smoothing: antialiased; 60+ -moz-osx-font-smoothing: grayscale; 61+ font-feature-settings: 'ss01', 'ss02', 'cv01'; 3262 } 3363 3464 body { 35- background-color: var(--color-bg); 36- color: var(--color-text-primary); 65+ background: var(--color-void); 66+ color: var(--color-ink); 3767 font-family: var(--font-sans); 68+ font-size: 14px; 69+ line-height: 1.65; 3870 } 3971 40-/* Graph-paper dot pattern */ 41-.graph-paper { 42- background-image: radial-gradient( 43- circle, 44- oklch(0.30 0.008 270) 0.5px, 45- transparent 0.5px 46- ); 47- background-size: 24px 24px; 72+::selection { 73+ background: oklch(0.45 0.12 260 / 0.4); 4874 } 4975 50-/* Noise texture overlay */ 51-.noise-overlay { 52- position: relative; 76+/* ── Grid texture: the mathematical substrate ── */ 77+.grid-texture { 78+ background-image: 79+ linear-gradient(to right, oklch(0.20 0.02 270 / 0.08) 1px, transparent 1px), 80+ linear-gradient(to bottom, oklch(0.20 0.02 270 / 0.08) 1px, transparent 1px); 81+ background-size: 40px 40px; 5382 } 54-.noise-overlay::after { 55- content: ''; 56- position: absolute; 57- inset: 0; 58- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); 59- opacity: 0.03; 60- pointer-events: none; 83+ 84+.dot-grid { 85+ background-image: radial-gradient(oklch(0.35 0.02 270 / 0.25) 1px, transparent 1px); 86+ background-size: 20px 20px; 6187 } 6288 63-/* Hide scrollbar for horizontal scroll areas while keeping scroll behavior */ 64-.scrollbar-none { 65- -ms-overflow-style: none; 66- scrollbar-width: none; 89+/* ── Glow effects ── */ 90+.glow-focus { 91+ box-shadow: 0 0 0 1px var(--color-focus-dim), 0 0 20px oklch(0.45 0.12 260 / 0.15); 6792 } 68-.scrollbar-none::-webkit-scrollbar { 69- display: none; 93+ 94+/* ── Scrollbar ── */ 95+.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; } 96+.scrollbar-none::-webkit-scrollbar { display: none; } 97+ 98+::-webkit-scrollbar { width: 6px; } 99+::-webkit-scrollbar-track { background: transparent; } 100+::-webkit-scrollbar-thumb { background: var(--color-line); border-radius: 3px; } 101+::-webkit-scrollbar-thumb:hover { background: var(--color-line-bright); } 102+ 103+/* ── Focus ring ── */ 104+:focus-visible { 105+ outline: 1.5px solid var(--color-focus); 106+ outline-offset: 2px; 70107 } 108+ 109+/* ── Graph paper (legacy util) ── */ 110+.graph-paper { background-image: radial-gradient(circle, oklch(0.30 0.008 270) 0.5px, transparent 0.5px); background-size: 24px 24px; } 111+.noise-overlay { position: relative; } 112+.noise-overlay::after { content: ''; position: absolute; inset: 0; opacity: 0.025; pointer-events: none; background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
@@ -18,49 +18,47 @@
1818 } 1919 </script> 2020 21-<header class="graph-paper border-b border-border bg-bg"> 22- <nav class="mx-auto flex h-12 max-w-6xl items-center justify-between px-4"> 23- <!-- Left: wordmark --> 24- <a href="/" class="flex items-center gap-1.5 text-base font-medium tracking-tight text-text-primary"> 25- <!-- Cospan arc: a subtle SVG arc above the 'o' evoking the cospan diagram apex --> 26- <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 27- <path d="M4 18 C4 18 8 4 12 4 C16 4 20 18 20 18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.6" /> 28- <circle cx="4" cy="18" r="2" fill="currentColor" opacity="0.4" /> 29- <circle cx="12" cy="4" r="2" fill="currentColor" opacity="0.7" /> 30- <circle cx="20" cy="18" r="2" fill="currentColor" opacity="0.4" /> 21+<header class="sticky top-0 z-50 border-b border-line/60 bg-void/80 backdrop-blur-xl"> 22+ <nav class="mx-auto flex h-14 max-w-[1200px] items-center justify-between px-6"> 23+ <!-- Wordmark --> 24+ <a href="/" class="group flex items-center gap-2 font-mono text-[15px] font-medium tracking-tight text-ink"> 25+ <!-- Cospan mark: three vertices, the apex brighter --> 26+ <svg class="h-[18px] w-[18px]" viewBox="0 0 18 18" fill="none"> 27+ <circle cx="3" cy="14" r="1.8" fill="currentColor" opacity="0.3"/> 28+ <circle cx="9" cy="3" r="2" fill="currentColor" opacity="0.7"/> 29+ <circle cx="15" cy="14" r="1.8" fill="currentColor" opacity="0.3"/> 30+ <line x1="3" y1="14" x2="9" y2="3" stroke="currentColor" stroke-width="0.8" opacity="0.2"/> 31+ <line x1="15" y1="14" x2="9" y2="3" stroke="currentColor" stroke-width="0.8" opacity="0.2"/> 3132 </svg> 32- <span>cospan</span> 33+ cospan 3334 </a> 3435 35- <!-- Center: nav links --> 36- <div class="flex items-center gap-6"> 36+ <!-- Nav --> 37+ <div class="flex items-center gap-1"> 3738 {#each navLinks as link} 3839 <a 3940 href={link.href} 40- class="relative text-sm transition-colors duration-150 41+ class="relative rounded-md px-3.5 py-1.5 text-[13px] font-medium transition-all duration-150 4142 {isActive(link.href) 42- ? 'text-accent' 43- : 'text-text-muted hover:text-text-secondary'}" 43+ ? 'bg-raised text-ink' 44+ : 'text-ghost hover:text-caption hover:bg-surface'}" 4445 > 4546 {link.label} 46- {#if isActive(link.href)} 47- <span class="absolute -bottom-[13px] left-0 right-0 h-[2px] bg-accent"></span> 48- {/if} 4947 </a> 5048 {/each} 5149 </div> 5250 53- <!-- Right: avatar or sign in --> 54- <div class="flex items-center gap-3"> 51+ <!-- Right --> 52+ <div class="flex items-center gap-2"> 5553 {#if user?.authenticated} 5654 <a 5755 href="/new" 58- class="rounded-md border border-border p-1.5 text-text-muted transition-colors hover:border-border-hover hover:text-text-secondary" 59- title="New repository" 56+ class="flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-[12px] font-medium text-caption transition-all hover:border-line-bright hover:text-ink" 6057 > 61- <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 58+ <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> 6259 <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 6360 </svg> 61+ New 6462 </a> 6563 {/if} 6664 <UserMenu {user} />
@@ -5,110 +5,74 @@
55 66 let displayName = $derived(repo.name || 'Untitled'); 77 8- // Known protocol hues; fallback uses a hash of the protocol name 9- const protocolHues: Record<string, number> = { 10- typescript: 230, 11- javascript: 50, 12- rust: 35, 13- python: 95, 14- go: 195, 15- java: 15, 16- kotlin: 280, 17- swift: 30, 18- ruby: 10, 19- csharp: 300, 20- protobuf: 145, 21- graphql: 330, 22- sql: 260, 23- atproto_lexicon: 250, 8+ const hueMap: Record<string, number> = { 9+ typescript: 230, javascript: 50, rust: 25, python: 100, 10+ go: 175, java: 10, kotlin: 275, swift: 15, ruby: 350, 11+ csharp: 290, protobuf: 145, graphql: 320, sql: 210, git: 15, 2412 }; 2513 26- function hashHue(value: string): number { 27- let hash = 0; 28- for (let i = 0; i < value.length; i++) { 29- hash = value.charCodeAt(i) + ((hash << 5) - hash); 30- } 31- return Math.abs(hash % 360); 14+ function hashHue(v: string): number { 15+ let h = 0; 16+ for (let i = 0; i < v.length; i++) h = v.charCodeAt(i) + ((h << 5) - h); 17+ return Math.abs(h % 360); 3218 } 3319 34- let hue = $derived(protocolHues[repo.protocol] ?? hashHue(repo.protocol)); 35- 36- // Owner handle: extract from DID for display (placeholder until handle resolution) 37- let ownerHandle = $derived(repo.did.startsWith('did:plc:') ? repo.did.slice(8, 20) + '...' : repo.did); 38- 20+ let hue = $derived(hueMap[repo.protocol] ?? hashHue(repo.protocol)); 21+ let ownerHandle = $derived( 22+ repo.did.startsWith('did:plc:') ? repo.did.slice(8, 18) + '…' : repo.did 23+ ); 3924 let isTangled = $derived(repo.source === 'tangled'); 4025 </script> 4126 4227 <a 4328 href="/{repo.did}/{repo.name}" 44- class="group block rounded-lg border-l-2 bg-surface-0 p-4 transition-all duration-200 hover:bg-surface-1" 45- style=" 46- border-left-color: oklch(0.45 0.12 {hue}); 47- background-color: oklch(0.14 0.015 {hue}); 48- " 49- onmouseenter={(e) => { 50- const el = e.currentTarget as HTMLElement; 51- el.style.borderLeftColor = `oklch(0.55 0.14 ${hue})`; 52- el.style.backgroundColor = `oklch(0.16 0.02 ${hue})`; 53- }} 54- onmouseleave={(e) => { 55- const el = e.currentTarget as HTMLElement; 56- el.style.borderLeftColor = `oklch(0.45 0.12 ${hue})`; 57- el.style.backgroundColor = `oklch(0.14 0.015 ${hue})`; 58- }} 29+ class="group relative flex flex-col rounded-lg border border-line/60 bg-ground p-4 transition-all duration-200 hover:border-line-bright hover:bg-surface" 5930 > 60- <!-- Top: owner handle + repo name --> 61- <div class="flex items-baseline gap-1.5"> 62- <span class="text-xs text-text-muted">{ownerHandle}</span> 63- <span class="text-xs text-text-muted">/</span> 64- <span class="font-mono text-sm font-medium text-text-primary">{displayName}</span> 31+ <!-- Protocol accent: left edge glow --> 32+ <div 33+ class="absolute left-0 top-3 bottom-3 w-[2px] rounded-full transition-opacity duration-200 group-hover:opacity-100" 34+ style="background: oklch(0.55 0.16 {hue}); opacity: 0.5;" 35+ ></div> 36+ 37+ <!-- Header --> 38+ <div class="flex items-center gap-1.5 pl-2.5"> 39+ <span class="text-[11px] text-ghost">{ownerHandle}</span> 40+ <span class="text-[11px] text-ghost/40">/</span> 41+ <span class="font-mono text-[13px] font-medium text-ink">{displayName}</span> 6542 </div> 6643 67- <!-- Middle: description (max 2 lines) --> 44+ <!-- Description --> 6845 {#if repo.description} 69- <p class="mt-2 line-clamp-2 text-sm leading-relaxed text-text-secondary"> 46+ <p class="mt-2 line-clamp-2 pl-2.5 text-[13px] leading-relaxed text-caption"> 7047 {repo.description} 7148 </p> 7249 {/if} 7350 74- <!-- Bottom row: protocol, stats, source badge --> 75- <div class="mt-3 flex items-center gap-3 text-xs"> 51+ <!-- Footer: metadata row --> 52+ <div class="mt-auto flex items-center gap-3 pt-3 pl-2.5 text-[11px] font-medium"> 53+ <!-- Protocol dot + name --> 7654 <span class="flex items-center gap-1.5"> 77- <span 78- class="inline-block h-2 w-2 rounded-full" 79- style="background-color: oklch(0.60 0.14 {hue})" 80- ></span> 81- <span style="color: oklch(0.60 0.14 {hue})">{repo.protocol}</span> 55+ <span class="h-[6px] w-[6px] rounded-full" style="background: oklch(0.60 0.16 {hue})"></span> 56+ <span class="text-ghost" style="color: oklch(0.55 0.10 {hue})">{repo.protocol}</span> 8257 </span> 8358 8459 {#if repo.starCount > 0} 85- <span class="text-text-muted" title="Stars"> 86- <svg class="mr-0.5 inline-block h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 87- <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.562.562 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.562.562 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> 88- </svg> 89- {repo.starCount} 90- </span> 91- {/if} 92- 93- {#if repo.forkCount > 0} 94- <span class="text-text-muted" title="Forks"> 95- <svg class="mr-0.5 inline-block h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 96- <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" /> 97- </svg> 98- {repo.forkCount} 60+ <span class="flex items-center gap-1 text-ghost" title="Stars"> 61+ ★ {repo.starCount} 9962 </span> 10063 {/if} 10164 10265 {#if repo.openIssueCount > 0} 103- <span class="text-text-muted" title="Open issues"> 66+ <span class="text-ghost" title="Open issues"> 10467 {repo.openIssueCount} issues 10568 </span> 10669 {/if} 10770 71+ <!-- Source badge (right-aligned) --> 10872 {#if isTangled} 109- <span class="ml-auto rounded-full bg-info/15 px-2 py-0.5 text-xs font-medium text-info">tangled</span> 110- {:else if repo.source && repo.source !== 'cospan'} 111- <span class="ml-auto rounded-full bg-surface-2 px-2 py-0.5 text-xs text-text-muted">{repo.source}</span> 73+ <span class="ml-auto rounded-full border border-info/20 bg-info/5 px-2 py-0.5 text-[10px] font-semibold text-info"> 74+ tangled 75+ </span> 11276 {/if} 11377 </div> 11478 </a>
@@ -12,9 +12,15 @@
1212 }); 1313 </script> 1414 15-<div class="flex min-h-screen flex-col"> 15+<div class="flex min-h-screen flex-col bg-void"> 1616 <Header user={auth.authenticated ? { authenticated: true, did: auth.did ?? '', handle: auth.handle ?? '', displayName: auth.displayName, avatar: auth.avatar } : null} /> 17- <main class="mx-auto w-full max-w-6xl flex-1 px-4 py-6"> 17+ <main class="mx-auto w-full max-w-[1200px] flex-1 px-6 py-6"> 1818 {@render children()} 1919 </main> 20+ <footer class="border-t border-line/50 py-8"> 21+ <div class="mx-auto max-w-[1200px] px-6 flex items-center justify-between text-xs text-ghost"> 22+ <span>cospan — schema-first code hosting</span> 23+ <span>built on <a href="https://atproto.com" class="text-caption hover:text-ink transition-colors">AT Protocol</a></span> 24+ </div> 25+ </footer> 2026 </div>
@@ -6,13 +6,16 @@
66 77 let { data } = $props(); 88 9- let activeProtocol = $derived(data.protocol); 109 let searchQuery = $state(''); 1110 let showDropdown = $state(false); 12- 13- // Source tab from URL 1411 let activeSource = $derived($page.url.searchParams.get('source') ?? 'all'); 1512 13+ // Multi-select protocols from URL 14+ let activeProtocols = $derived<string[]>(() => { 15+ const p = $page.url.searchParams.get('protocol'); 16+ return p ? p.split(',').filter(Boolean) : []; 17+ }); 18+ 1619 let filtered = $derived( 1720 searchQuery.trim() 1821 ? ALL_LANGUAGES.filter((l) =>
@@ -22,10 +25,28 @@
2225 : ALL_LANGUAGES.slice(0, 20) 2326 ); 2427 25- function selectProtocol(value: string) { 28+ function toggleProtocol(value: string) { 29+ const current = activeProtocols(); 30+ const next = current.includes(value) 31+ ? current.filter((p) => p !== value) 32+ : [...current, value]; 2633 showDropdown = false; 2734 searchQuery = ''; 28- goto(`/?protocol=${value}`); 35+ const params = new URLSearchParams($page.url.searchParams); 36+ if (next.length === 0) { 37+ params.delete('protocol'); 38+ } else { 39+ params.set('protocol', next.join(',')); 40+ } 41+ const qs = params.toString(); 42+ goto(qs ? `/?${qs}` : '/', { noScroll: true, replaceState: true }); 43+ } 44+ 45+ function clearProtocols() { 46+ const params = new URLSearchParams($page.url.searchParams); 47+ params.delete('protocol'); 48+ const qs = params.toString(); 49+ goto(qs ? `/?${qs}` : '/', { noScroll: true, replaceState: true }); 2950 } 3051 3152 const sourceTabs = [
@@ -36,97 +57,144 @@
3657 3758 function setSource(source: string) { 3859 const params = new URLSearchParams($page.url.searchParams); 39- if (source === 'all') { 40- params.delete('source'); 41- } else { 42- params.set('source', source); 43- } 60+ if (source === 'all') params.delete('source'); 61+ else params.set('source', source); 4462 const qs = params.toString(); 45- goto(qs ? `/?${qs}` : '/'); 63+ goto(qs ? `/?${qs}` : '/', { noScroll: true, replaceState: true }); 4664 } 4765 48- // Generate a deterministic color from language name 4966 function langColor(value: string): string { 5067 let hash = 0; 51- for (let i = 0; i < value.length; i++) { 52- hash = value.charCodeAt(i) + ((hash << 5) - hash); 53- } 54- const hue = Math.abs(hash % 360); 55- return `oklch(0.65 0.15 ${hue})`; 68+ for (let i = 0; i < value.length; i++) hash = value.charCodeAt(i) + ((hash << 5) - hash); 69+ return `oklch(0.65 0.15 ${Math.abs(hash % 360)})`; 5670 } 71+ 72+ let totalRepos = $derived(data.trending.items.length + data.recent.items.length); 73+ let activeView = $state<'trending' | 'recent'>('trending'); 74+ let activeItems = $derived(activeView === 'trending' ? data.trending.items : data.recent.items); 75+ let hasAnyRepos = $derived(data.trending.items.length > 0 || data.recent.items.length > 0); 5776 </script> 5877 5978 <svelte:head> 60- <title>Cospan</title> 79+ <title>Cospan | Schematic code hosting</title> 6180 </svelte:head> 6281 63-<!-- Hero section --> 64-<section class="noise-overlay graph-paper relative -mx-4 -mt-6 mb-8 px-4 py-16" style="background: linear-gradient(180deg, oklch(0.14 0.02 270) 0%, oklch(0.11 0.008 270) 100%);"> 65- <div class="relative mx-auto max-w-6xl"> 66- <div class="flex items-start justify-between"> 67- <div> 68- <h1 class="text-[28px] font-medium leading-tight text-text-primary"> 69- Schema-first code hosting. 70- </h1> 71- <p class="mt-3 text-[15px] leading-relaxed text-text-secondary"> 72- Structural diffs. Algebraic CI. Breaking change detection. 73- </p> 74- <p class="mt-4 text-xs text-text-muted">Built on AT Protocol</p> 75- </div> 82+<!-- ═══ HERO ═══ --> 83+<section class="relative -mx-6 -mt-6 overflow-hidden border-b border-line/40"> 84+ <!-- Ambient grid --> 85+ <div class="dot-grid pointer-events-none absolute inset-0 opacity-60"></div> 7686 77- <!-- Abstract cospan diagram decoration --> 78- <svg class="hidden h-24 w-40 md:block" viewBox="0 0 160 96" fill="none" xmlns="http://www.w3.org/2000/svg"> 79- <!-- Connecting lines --> 80- <line x1="30" y1="70" x2="80" y2="26" stroke="oklch(0.40 0.01 270)" stroke-width="1" opacity="0.2" /> 81- <line x1="130" y1="70" x2="80" y2="26" stroke="oklch(0.40 0.01 270)" stroke-width="1" opacity="0.2" /> 82- <line x1="30" y1="70" x2="50" y2="80" stroke="oklch(0.40 0.01 270)" stroke-width="1" opacity="0.15" /> 83- <line x1="130" y1="70" x2="110" y2="80" stroke="oklch(0.40 0.01 270)" stroke-width="1" opacity="0.15" /> 84- <!-- Vertices --> 85- <circle cx="80" cy="26" r="5" fill="oklch(0.50 0.08 250)" opacity="0.25" /> 86- <circle cx="30" cy="70" r="4" fill="oklch(0.45 0.06 250)" opacity="0.20" /> 87- <circle cx="130" cy="70" r="4" fill="oklch(0.45 0.06 250)" opacity="0.20" /> 88- <circle cx="50" cy="80" r="3" fill="oklch(0.40 0.04 250)" opacity="0.15" /> 89- <circle cx="110" cy="80" r="3" fill="oklch(0.40 0.04 250)" opacity="0.15" /> 87+ <!-- Gradient atmosphere --> 88+ <div class="pointer-events-none absolute inset-0" style="background: radial-gradient(ellipse 80% 60% at 50% 0%, oklch(0.18 0.04 260 / 0.5), transparent 70%);"></div> 89+ 90+ <div class="relative mx-auto max-w-[1200px] px-6 pb-20 pt-24"> 91+ <div class="max-w-2xl"> 92+ <!-- Headline --> 93+ <h1 class="text-[clamp(2rem,5vw,3.25rem)] font-semibold leading-[1.1] tracking-tight text-ink"> 94+ Code hosting with<br> 95+ <span class="text-caption">schematic version control.</span> 96+ </h1> 97+ 98+ <!-- Description --> 99+ <p class="mt-6 max-w-lg text-[15px] leading-relaxed text-caption"> 100+ Structural diffs, schema-aware merges, and algebraic validation powered by panproto. Built on AT Protocol. 101+ </p> 102+ 103+ </div> 104+ 105+ <!-- Cospan diagram: abstract decorative element --> 106+ <div class="pointer-events-none absolute right-6 top-20 hidden opacity-[0.07] lg:block"> 107+ <svg width="400" height="300" viewBox="0 0 400 300" fill="none"> 108+ <!-- Large cospan: two morphisms meeting at apex --> 109+ <circle cx="200" cy="50" r="20" stroke="currentColor" stroke-width="1"/> 110+ <circle cx="60" cy="240" r="14" stroke="currentColor" stroke-width="0.8"/> 111+ <circle cx="340" cy="240" r="14" stroke="currentColor" stroke-width="0.8"/> 112+ <line x1="60" y1="240" x2="200" y2="50" stroke="currentColor" stroke-width="0.6"/> 113+ <line x1="340" y1="240" x2="200" y2="50" stroke="currentColor" stroke-width="0.6"/> 114+ 115+ <!-- Secondary structure: a pushout square --> 116+ <circle cx="130" cy="145" r="8" stroke="currentColor" stroke-width="0.5"/> 117+ <circle cx="270" cy="145" r="8" stroke="currentColor" stroke-width="0.5"/> 118+ <line x1="130" y1="145" x2="200" y2="50" stroke="currentColor" stroke-width="0.4" stroke-dasharray="4 4"/> 119+ <line x1="270" y1="145" x2="200" y2="50" stroke="currentColor" stroke-width="0.4" stroke-dasharray="4 4"/> 120+ <line x1="130" y1="145" x2="60" y2="240" stroke="currentColor" stroke-width="0.4" stroke-dasharray="4 4"/> 121+ <line x1="270" y1="145" x2="340" y2="240" stroke="currentColor" stroke-width="0.4" stroke-dasharray="4 4"/> 90122 </svg> 91123 </div> 92124 </div> 93125 </section> 94126 95-<!-- Source tabs + language filter --> 96-<section class="mx-auto max-w-6xl"> 97- <div class="mb-6 flex flex-wrap items-center justify-between gap-4"> 98- <!-- Source tabs --> 99- <div class="flex items-center gap-1"> 100- {#each sourceTabs as tab} 127+<!-- ═══ CONTROLS ═══ --> 128+<section> 129+ <div class="flex flex-wrap items-center justify-between gap-4 border-b border-line/40 py-4"> 130+ <div class="flex items-center gap-3"> 131+ <!-- Source tabs --> 132+ <div class="flex items-center gap-0.5 rounded-lg bg-surface p-1"> 133+ {#each sourceTabs as tab} 134+ <button 135+ type="button" 136+ onclick={() => setSource(tab.value)} 137+ class="rounded-md px-3.5 py-1.5 text-[12px] font-medium transition-all duration-150 138+ {activeSource === tab.value 139+ ? 'bg-raised text-ink shadow-sm' 140+ : 'text-ghost hover:text-caption'}" 141+ > 142+ {tab.label} 143+ </button> 144+ {/each} 145+ </div> 146+ 147+ <div class="h-5 w-px bg-line/60"></div> 148+ 149+ <!-- View tabs: Trending / Recent --> 150+ <div class="flex items-center gap-0.5 rounded-lg bg-surface p-1"> 101151 <button 102152 type="button" 103- onclick={() => setSource(tab.value)} 104- class="relative px-3 py-1.5 text-sm transition-colors duration-150 105- {activeSource === tab.value 106- ? 'text-accent' 107- : 'text-text-muted hover:text-text-secondary'}" 153+ onclick={() => activeView = 'trending'} 154+ class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium transition-all 155+ {activeView === 'trending' ? 'bg-raised text-ink shadow-sm' : 'text-ghost hover:text-caption'}" 108156 > 109- {tab.label} 110- {#if activeSource === tab.value} 111- <span class="absolute bottom-0 left-0 right-0 h-[2px] bg-accent"></span> 112- {/if} 157+ <svg class="h-3 w-3 {activeView === 'trending' ? 'text-warn' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> 158+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941" /> 159+ </svg> 160+ Trending 113161 </button> 114- {/each} 162+ <button 163+ type="button" 164+ onclick={() => activeView = 'recent'} 165+ class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[12px] font-medium transition-all 166+ {activeView === 'recent' ? 'bg-raised text-ink shadow-sm' : 'text-ghost hover:text-caption'}" 167+ > 168+ <svg class="h-3 w-3 {activeView === 'recent' ? 'text-focus' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> 169+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> 170+ </svg> 171+ Recent 172+ </button> 173+ </div> 115174 </div> 116175 117- <!-- Language filter --> 176+ <!-- Language filter (multiselect) --> 118177 <div class="flex items-center gap-2"> 119- {#if activeProtocol} 120- <span class="flex items-center gap-1.5 rounded-full border border-accent/30 bg-accent/5 px-2.5 py-1 text-xs text-accent"> 121- <span class="h-1.5 w-1.5 rounded-full" style="background-color: {langColor(activeProtocol)}"></span> 122- {ALL_LANGUAGES.find((l) => l.value === activeProtocol)?.label ?? activeProtocol} 123- <a href="/" class="ml-0.5 text-text-muted hover:text-text-primary" title="Clear language filter"> 124- <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 125- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> 126- </svg> 127- </a> 128- </span> 178+ {#if activeProtocols().length > 1} 179+ <button 180+ type="button" 181+ onclick={clearProtocols} 182+ class="text-[11px] text-ghost hover:text-caption transition-colors" 183+ > 184+ Clear all 185+ </button> 129186 {/if} 187+ {#each activeProtocols() as proto} 188+ <button 189+ type="button" 190+ onclick={() => toggleProtocol(proto)} 191+ class="flex items-center gap-1.5 rounded-full border border-focus/30 bg-focus/5 px-2.5 py-1 text-[11px] font-medium text-focus-bright transition-colors hover:bg-focus/10" 192+ > 193+ <span class="h-1.5 w-1.5 rounded-full" style="background-color: {langColor(proto)}"></span> 194+ {ALL_LANGUAGES.find((l) => l.value === proto)?.label ?? proto} 195+ <span class="ml-0.5 text-ghost">×</span> 196+ </button> 197+ {/each} 130198 131199 <div class="relative"> 132200 <input
@@ -134,23 +202,35 @@
134202 bind:value={searchQuery} 135203 onfocus={() => showDropdown = true} 136204 onblur={() => setTimeout(() => showDropdown = false, 200)} 137- placeholder="Filter by language..." 205+ onkeydown={(e) => { 206+ if (e.key === 'Enter' && filtered.length > 0) { 207+ e.preventDefault(); 208+ toggleProtocol(filtered[0].value); 209+ } 210+ }} 211+ placeholder="Filter by language…" 138212 autocomplete="off" 139213 spellcheck="false" 140- class="w-44 rounded-md border border-border bg-surface-0 px-3 py-1.5 text-xs text-text-primary placeholder:text-text-muted focus:border-accent focus:outline-none" 214+ class="w-40 rounded-md border border-line bg-surface px-3 py-1.5 text-[12px] text-ink placeholder:text-ghost 215+ focus:border-focus/50 focus:outline-none transition-colors" 141216 /> 142- 143- {#if showDropdown} 144- <ul class="absolute right-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"> 217+ {#if showDropdown && filtered.length > 0} 218+ <ul class="absolute right-0 top-full z-50 mt-1 max-h-52 w-56 overflow-y-auto rounded-lg border border-line bg-raised shadow-xl shadow-black/40"> 145219 {#each filtered as lang} 146220 <li> 147221 <button 148222 type="button" 149- onmousedown={() => selectProtocol(lang.value)} 150- class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-surface-2 151- {activeProtocol === lang.value ? 'text-accent' : 'text-text-muted'}" 223+ onmousedown={() => toggleProtocol(lang.value)} 224+ class="flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] transition-colors hover:bg-elevated 225+ {activeProtocols().includes(lang.value) ? 'text-focus-bright' : 'text-caption'}" 152226 > 153- <span class="h-1.5 w-1.5 shrink-0 rounded-full" style="background-color: {langColor(lang.value)}"></span> 227+ {#if activeProtocols().includes(lang.value)} 228+ <svg class="h-3 w-3 text-focus" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"> 229+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /> 230+ </svg> 231+ {:else} 232+ <span class="h-1.5 w-1.5 shrink-0 rounded-full" style="background-color: {langColor(lang.value)}"></span> 233+ {/if} 154234 {lang.label} 155235 </button> 156236 </li>
@@ -160,49 +240,27 @@
160240 </div> 161241 </div> 162242 </div> 243+</section> 163244 164- <!-- Trending section --> 165- <div class="mb-10"> 166- <div class="mb-4 flex items-center gap-2"> 167- <svg class="h-4 w-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 168- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941" /> 169- </svg> 170- <h2 class="text-sm font-medium text-text-primary">Trending</h2> 171- <span class="text-xs text-text-muted">by recent stars</span> 172- </div> 173- 174- {#if data.trending.items.length === 0} 175- <p class="py-12 text-center text-sm text-text-muted"> 176- {activeProtocol ? `No ${activeProtocol} repositories found.` : 'No repositories yet.'} 177- </p> 178- {:else} 179- <div class="grid gap-4" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));"> 180- {#each data.trending.items as repo (repo.did + '/' + repo.name)} 181- <RepoCard {repo} /> 182- {/each} 245+<!-- ═══ REPOS ═══ --> 246+<section class="py-8"> 247+ {#if activeItems.length === 0} 248+ <div class="flex flex-col items-center justify-center py-24 text-center"> 249+ <div class="mb-4 text-ghost"> 250+ <svg class="mx-auto h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1"> 251+ <path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> 252+ </svg> 183253 </div> 184- {/if} 185- </div> 186- 187- <!-- Recently updated section --> 188- <div> 189- <div class="mb-4 flex items-center gap-2"> 190- <svg class="h-4 w-4 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 191- <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> 192- </svg> 193- <h2 class="text-sm font-medium text-text-primary">Recently Updated</h2> 194- </div> 195- 196- {#if data.recent.items.length === 0} 197- <p class="py-12 text-center text-sm text-text-muted"> 198- No recently updated repositories. 254+ <p class="text-sm text-caption"> 255+ {activeProtocols().length > 0 ? `No repositories found for selected languages.` : 'No repositories yet.'} 199256 </p> 200- {:else} 201- <div class="grid gap-4" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));"> 202- {#each data.recent.items as repo (repo.did + '/' + repo.name)} 203- <RepoCard {repo} /> 204- {/each} 205- </div> 206- {/if} 207- </div> 257+ <p class="mt-1 text-xs text-ghost">Repositories from Cospan and Tangled will appear here.</p> 258+ </div> 259+ {:else} 260+ <div class="grid gap-3" style="grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));"> 261+ {#each activeItems as repo (repo.did + '/' + repo.name)} 262+ <RepoCard {repo} /> 263+ {/each} 264+ </div> 265+ {/if} 208266 </section>