feat: reskin design system + Tangled source tracking + import endpoint Backend: - Fix source field: Tangled repos now get source="tangled" with source_uri - Add source filter to repo list (?source=tangled|cospan) - Add dev.cospan.repo.import procedure for importing Tangled repos - New Lexicon: dev.cospan.repo.import Frontend design system: - New blue-tinted dark palette (oklch hue 270, blueprinty mathematical feel) - Graph-paper dot grid texture utility class - Noise overlay for depth - Three-level text hierarchy (primary/secondary/muted) - Protocol-specific card tinting (14 protocol hues) Components: - Header: minimal single-line with cospan arc icon, nav links, no hamburger - RepoCard: protocol-tinted background, left border accent, hover transitions - Landing page: hero with gradient + graph-paper + cospan SVG diagram, source tabs (All/Cospan/Tangled), responsive card grid API types: - Repo: added source, forkCount, sourceUri, sourceRepo, nodeDid, nodeUrl - RefUpdate: added algebraicChecks, lensId

Author: Aaron Steven White
Commit a09383ac462ab9905a0d94e83666cfb11ac31699
Parent: c6b1b763a5
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
34 files changed +808 -668
@@ -1,28 +1,65 @@
11 @import "tailwindcss";
22 
33 @theme {
4-	--color-surface-0: oklch(0.13 0.01 260);
5-	--color-surface-1: oklch(0.16 0.01 260);
6-	--color-surface-2: oklch(0.20 0.01 260);
7-	--color-border: oklch(0.28 0.01 260);
8-	--color-accent: oklch(0.65 0.15 250);
9-	--color-accent-hover: oklch(0.70 0.15 250);
10-	--color-text-primary: oklch(0.93 0.01 260);
11-	--color-text-secondary: oklch(0.65 0.01 260);
12-	--color-breaking: oklch(0.65 0.20 25);
13-	--color-compatible: oklch(0.65 0.17 145);
14-	--color-conflict: oklch(0.70 0.18 55);
15-	--color-lens: oklch(0.65 0.15 280);
16-	--font-sans: "Inter Variable", system-ui, sans-serif;
17-	--font-mono: "JetBrains Mono", "Fira Code", monospace;
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);
16+
17+	/* 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);
22+
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);
28+
29+	/* Typography */
30+	--font-sans: 'Inter Variable', 'Inter', system-ui, sans-serif;
31+	--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
1832 }
1933 
2034 body {
21-	background-color: var(--color-surface-0);
35+	background-color: var(--color-bg);
2236 	color: var(--color-text-primary);
2337 	font-family: var(--font-sans);
2438 }
2539 
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;
48+}
49+
50+/* Noise texture overlay */
51+.noise-overlay {
52+	position: relative;
53+}
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;
61+}
62+
2663 /* Hide scrollbar for horizontal scroll areas while keeping scroll behavior */
2764 .scrollbar-none {
2865 	-ms-overflow-style: none;
@@ -1,5 +1,12 @@
11 import { xrpcQuery } from './client.js';
22 
3+export interface AlgebraicChecks {
4+	gatTypeCheck: string | null;
5+	equationVerification: string | null;
6+	lensLawCheck: string | null;
7+	breakingChangeCheck: string | null;
8+}
9+
310 export interface RefUpdate {
411 	rkey: string;
512 	repo: string;
@@ -9,8 +16,10 @@ export interface RefUpdate {
916 	protocol: string;
1017 	commitCount: number;
1118 	migrationId: string | null;
19+	lensId: string | null;
1220 	lensQuality: number | null;
1321 	breakingChangeCount: number;
22+	algebraicChecks: AlgebraicChecks | null;
1423 	committerDid: string;
1524 	committerHandle: string | null;
1625 	createdAt: string;
@@ -6,8 +6,16 @@ export interface Repo {
66 	description: string | null;
77 	protocol: string;
88 	starCount: number;
9+	forkCount: number;
910 	openIssueCount: number;
1011 	openMrCount: number;
12+	source: string;
13+	sourceUri: string | null;
14+	sourceRepo: string | null;
15+	nodeDid: string;
16+	nodeUrl: string;
17+	defaultBranch: string;
18+	visibility: string;
1119 	createdAt: string;
1220 }
1321 
@@ -23,6 +31,7 @@ interface RawRepoListResponse {
2331 
2432 export async function listRepos(params?: {
2533 	did?: string;
34+	source?: string;
2635 	limit?: number;
2736 	cursor?: string;
2837 }): Promise<RepoListResponse> {
@@ -9,19 +9,18 @@
99 
1010 <a
1111 	href="{basePath}/issues/{issue.rkey}"
12-	class="block rounded-lg border border-border bg-surface-1 p-4 transition-colors hover:border-accent"
12+	class="block rounded-lg border border-border bg-surface-1 p-4 transition-all hover:border-border-hover"
1313 >
1414 	<div class="flex items-start gap-3">
1515 		<div class="mt-0.5">
1616 			<StateBadge state={issue.state} />
1717 		</div>
1818 		<div class="min-w-0 flex-1">
19-			<h3 class="font-medium text-text-primary">{issue.title}</h3>
20-			<div class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary">
19+			<h3 class="font-semibold text-text-primary">{issue.title}</h3>
20+			<div class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-muted">
2121 				<span>#{issue.rkey}</span>
22-				<span>opened {timeAgo(issue.createdAt)}</span>
2322 				{#if issue.creatorHandle}
24-					<span>by {issue.creatorHandle}</span>
23+					<span>opened by {issue.creatorHandle}</span>
2524 				{/if}
2625 				{#if issue.commentCount > 0}
2726 					<span class="flex items-center gap-1">
@@ -31,6 +30,7 @@
3130 						{issue.commentCount}
3231 					</span>
3332 				{/if}
33+				<span>{timeAgo(issue.createdAt)}</span>
3434 			</div>
3535 			{#if issue.labels.length > 0}
3636 				<div class="mt-2 flex flex-wrap gap-1">
@@ -1,122 +1,61 @@
11 <script lang="ts">
2-	import { goto } from '$app/navigation';
32 	import { page } from '$app/stores';
4-	import { debounce } from '$lib/utils/debounce.js';
53 	import UserMenu from '$lib/components/auth/UserMenu.svelte';
6-	import NotificationBell from '$lib/components/shared/NotificationBell.svelte';
74 
85 	let { user }: { user?: { authenticated: boolean; did: string; handle: string; displayName?: string; avatar?: string } | null } = $props();
96 
10-	let searchValue = $state('');
11-	let mobileSearchValue = $state('');
12-	let searchInputRef = $state<HTMLInputElement | null>(null);
13-	let mobileMenuOpen = $state(false);
7+	let currentPath = $derived($page.url.pathname);
148 
15-	// Close mobile menu on navigation
16-	$effect(() => {
17-		$page.url.pathname;
18-		mobileMenuOpen = false;
19-	});
9+	const navLinks = [
10+		{ href: '/', label: 'Explore' },
11+		{ href: '/feed', label: 'Feed' },
12+		{ href: '/search', label: 'Search' },
13+	] as const;
2014 
21-	const submitSearch = debounce((q: string) => {
22-		if (q.trim()) {
23-			goto(`/search?q=${encodeURIComponent(q.trim())}`);
24-		}
25-	}, 300);
26-
27-	function handleSearchKeydown(event: KeyboardEvent) {
28-		if (event.key === 'Enter' && searchValue.trim()) {
29-			goto(`/search?q=${encodeURIComponent(searchValue.trim())}`);
30-		}
31-	}
32-
33-	function handleMobileSearchKeydown(event: KeyboardEvent) {
34-		if (event.key === 'Enter' && mobileSearchValue.trim()) {
35-			goto(`/search?q=${encodeURIComponent(mobileSearchValue.trim())}`);
36-			mobileMenuOpen = false;
37-		}
38-	}
39-
40-	function handleGlobalKeydown(event: KeyboardEvent) {
41-		const target = event.target as HTMLElement;
42-		if (
43-			target.tagName === 'INPUT' ||
44-			target.tagName === 'TEXTAREA' ||
45-			target.isContentEditable
46-		) {
47-			return;
48-		}
49-
50-		if (event.key === '/') {
51-			event.preventDefault();
52-			searchInputRef?.focus();
53-		}
54-
55-		if (event.key === 'Escape' && mobileMenuOpen) {
56-			mobileMenuOpen = false;
57-		}
58-	}
59-
60-	function toggleMobileMenu() {
61-		mobileMenuOpen = !mobileMenuOpen;
15+	function isActive(href: string): boolean {
16+		if (href === '/') return currentPath === '/';
17+		return currentPath.startsWith(href);
6218 	}
6319 </script>
6420 
65-<svelte:window onkeydown={handleGlobalKeydown} />
66-
67-<header class="border-b border-border bg-surface-1">
21+<header class="graph-paper border-b border-border bg-bg">
6822 	<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" />
31+			</svg>
32+			<span>cospan</span>
33+		</a>
34+
35+		<!-- Center: nav links -->
6936 		<div class="flex items-center gap-6">
70-			<a href="/" class="flex items-center gap-2 text-lg font-semibold tracking-tight text-text-primary">
71-				<img src="/logo-dark.svg" alt="" class="h-6 w-6" />
72-				<span class="hidden xs:inline">Cospan</span>
73-			</a>
74-			<div class="hidden items-center gap-4 md:flex">
75-				<a href="/" class="text-sm text-text-secondary hover:text-text-primary transition-colors">
76-					Explore
77-				</a>
78-				<a href="/feed" class="text-sm text-text-secondary hover:text-text-primary transition-colors">
79-					Feed
80-				</a>
81-				<a href="/orgs" class="text-sm text-text-secondary hover:text-text-primary transition-colors">
82-					Orgs
83-				</a>
84-				<a href="/search" class="text-sm text-text-secondary hover:text-text-primary transition-colors">
85-					Search
37+			{#each navLinks as link}
38+				<a
39+					href={link.href}
40+					class="relative text-sm transition-colors duration-150
41+						{isActive(link.href)
42+							? 'text-accent'
43+							: 'text-text-muted hover:text-text-secondary'}"
44+				>
45+					{link.label}
46+					{#if isActive(link.href)}
47+						<span class="absolute -bottom-[13px] left-0 right-0 h-[2px] bg-accent"></span>
48+					{/if}
8649 				</a>
87-			</div>
50+			{/each}
8851 		</div>
52+
53+		<!-- Right: avatar or sign in -->
8954 		<div class="flex items-center gap-3">
90-			<!-- Desktop search -->
91-			<div class="relative hidden md:block">
92-				<svg
93-					class="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-secondary"
94-					fill="none"
95-					viewBox="0 0 24 24"
96-					stroke="currentColor"
97-					stroke-width="2"
98-				>
99-					<path
100-						stroke-linecap="round"
101-						stroke-linejoin="round"
102-						d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
103-					/>
104-				</svg>
105-				<input
106-					bind:this={searchInputRef}
107-					type="text"
108-					bind:value={searchValue}
109-					oninput={() => submitSearch(searchValue)}
110-					onkeydown={handleSearchKeydown}
111-					placeholder="Search... (/)"
112-					class="w-48 rounded-md border border-border bg-surface-0 py-1 pl-8 pr-3 text-xs text-text-primary placeholder:text-text-secondary focus:border-accent focus:outline-none"
113-				/>
114-			</div>
115-			<NotificationBell />
11655 			{#if user?.authenticated}
11756 				<a
11857 					href="/new"
119-					class="hidden rounded-md border border-border bg-surface-1 p-1.5 text-text-secondary transition-colors hover:border-accent hover:text-text-primary sm:block"
58+					class="rounded-md border border-border p-1.5 text-text-muted transition-colors hover:border-border-hover hover:text-text-secondary"
12059 					title="New repository"
12160 				>
12261 					<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -124,105 +63,7 @@
12463 					</svg>
12564 				</a>
12665 			{/if}
127-			<div class="hidden sm:block">
128-				<UserMenu {user} />
129-			</div>
130-
131-			<!-- Mobile hamburger -->
132-			<button
133-				onclick={toggleMobileMenu}
134-				class="rounded-md p-1.5 text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary md:hidden"
135-				aria-label="Toggle menu"
136-			>
137-				{#if mobileMenuOpen}
138-					<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
139-						<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
140-					</svg>
141-				{:else}
142-					<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
143-						<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
144-					</svg>
145-				{/if}
146-			</button>
66+			<UserMenu {user} />
14767 		</div>
14868 	</nav>
149-
150-	<!-- Mobile menu -->
151-	{#if mobileMenuOpen}
152-		<div class="border-t border-border bg-surface-1 px-4 py-3 md:hidden">
153-			<!-- Mobile search -->
154-			<div class="mb-3">
155-				<div class="relative">
156-					<svg
157-						class="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-secondary"
158-						fill="none"
159-						viewBox="0 0 24 24"
160-						stroke="currentColor"
161-						stroke-width="2"
162-					>
163-						<path
164-							stroke-linecap="round"
165-							stroke-linejoin="round"
166-							d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
167-						/>
168-					</svg>
169-					<input
170-						type="text"
171-						bind:value={mobileSearchValue}
172-						onkeydown={handleMobileSearchKeydown}
173-						placeholder="Search repositories..."
174-						class="w-full rounded-md border border-border bg-surface-0 py-2 pl-8 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:border-accent focus:outline-none"
175-					/>
176-				</div>
177-			</div>
178-
179-			<!-- Mobile nav links -->
180-			<div class="space-y-1">
181-				<a href="/" class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary">
182-					Explore
183-				</a>
184-				<a href="/feed" class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary">
185-					Feed
186-				</a>
187-				<a href="/orgs" class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary">
188-					Orgs
189-				</a>
190-				<a href="/search" class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary">
191-					Search
192-				</a>
193-			</div>
194-
195-			{#if user?.authenticated}
196-				<div class="mt-3 border-t border-border pt-3 space-y-1">
197-					<a
198-						href="/new"
199-						class="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary"
200-					>
201-						<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
202-							<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
203-						</svg>
204-						New repository
205-					</a>
206-					<a
207-						href="/{user.did}"
208-						class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary"
209-					>
210-						Your profile
211-					</a>
212-					<a
213-						href="/settings"
214-						class="block rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-2 hover:text-text-primary"
215-					>
216-						Settings
217-					</a>
218-				</div>
219-			{:else}
220-				<div class="mt-3 border-t border-border pt-3">
221-					<div class="sm:hidden">
222-						<UserMenu {user} />
223-					</div>
224-				</div>
225-			{/if}
226-		</div>
227-	{/if}
22869 </header>
cospan · schematic version control on atproto built on AT Protocol