this repo has no description
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(profile): branded inline-SVG icons + account provider name

Public profile button changes:

- Add inline-SVG icon components for Bluesky (butterfly), Tangled
(dolly mark), and Website (globe). Each uses currentColor so it
picks up the site's primary blue (#254a9e) instead of the brand
colour baked into a favicon. Alt Bluesky clients (Blacksky,
Witchsky, etc.) keep their favicons so they remain distinguishable.
- ResolvedLink gets an optional iconKind tag; ProfileLinks switches
on it to render the right inline SVG, falling back to the favicon
iconUrl, then a glyph.
- Drop the URL subtitle from atmosphere + website buttons. The title
alone is the affordance; the URL is just the destination.

Footer:

- 'Hosted on:' → 'Account Provider:', sourced from a new
lib/account-providers.ts mapper. Per-shard PDS hosts like
shimeji.us-east.host.bsky.network now collapse to "Bluesky".
Self-hosted PDSes still show the bare hostname.

Made-with: Cursor

+237 -39
+19 -7
assets/styles.css
··· 2146 2146 font-size: 1rem; 2147 2147 padding: 0; 2148 2148 } 2149 + /* Wrapper for inline-SVG branded icons (Bluesky / Tangled / Website 2150 + * globe). Tints the SVG to the site's primary blue via currentColor. 2151 + * On the primary CTA we invert to white so the icon stays legible 2152 + * against the blue gradient background. */ 2153 + .profile-action-icon--brand { 2154 + display: inline-flex; 2155 + align-items: center; 2156 + justify-content: center; 2157 + color: #254a9e; 2158 + padding: 5px; 2159 + } 2160 + .profile-action-icon-svg { 2161 + width: 100%; 2162 + height: 100%; 2163 + } 2164 + .profile-action--primary .profile-action-icon--brand { 2165 + color: #1d3a78; 2166 + background: #ffffff; 2167 + } 2149 2168 .profile-action-label { 2150 2169 display: flex; 2151 2170 flex-direction: column; ··· 2155 2174 .profile-action-title { 2156 2175 font-weight: 600; 2157 2176 font-size: 0.95rem; 2158 - } 2159 - .profile-action-sub { 2160 - font-family: "IBM Plex Mono", monospace; 2161 - font-size: 0.75rem; 2162 - letter-spacing: 0.02em; 2163 - opacity: 0.75; 2164 - word-break: break-word; 2165 2177 } 2166 2178 .dark-phase .profile-action { 2167 2179 background: rgba(255, 255, 255, 0.1);
+61 -22
components/explore/ProfileLinks.tsx
··· 1 1 import type { ProfileRow } from "../../lib/registry.ts"; 2 - import { resolveLink } from "../../lib/atmosphere-links.ts"; 2 + import { resolveLink, type ResolvedIconKind } from "../../lib/atmosphere-links.ts"; 3 3 import { useT } from "../../i18n/mod.ts"; 4 + import BskyIcon from "../icons/BskyIcon.tsx"; 5 + import TangledIcon from "../icons/TangledIcon.tsx"; 6 + import WebsiteIcon from "../icons/WebsiteIcon.tsx"; 4 7 5 8 interface Props { 6 9 profile: ProfileRow; ··· 9 12 /** 10 13 * Renders the public profile's action buttons. We iterate `profile.links` 11 14 * in author-defined order and resolve each entry to a render-ready 12 - * bundle via `resolveLink` (which knows about atmosphere kinds, custom 13 - * websites, etc.). The handle is passed in so atmosphere kinds can 14 - * derive their default URL from it. 15 + * bundle via `resolveLink`. The resolver tags each link with an 16 + * optional `iconKind` so we can render the on-brand inline SVG (which 17 + * inherits the site's blue via currentColor) for known services, while 18 + * still falling back to favicons / glyphs for everything else. 15 19 * 16 - * Buttons are visually consistent — the first one in the list naturally 17 - * becomes the "primary" CTA via the `:first-child` selector in CSS. 20 + * URL subtitles are intentionally hidden for atmosphere services and 21 + * the website button — the title alone is enough; the URL is a 22 + * destination, not metadata. Custom links keep their subtitle so the 23 + * user knows where they're going. 18 24 */ 19 25 export default function ProfileLinks({ profile }: Props) { 20 26 const t = useT(); ··· 36 42 rel="noopener noreferrer" 37 43 key={`${r.href}-${i}`} 38 44 > 39 - {r.iconUrl 40 - ? ( 41 - <img 42 - src={r.iconUrl} 43 - alt="" 44 - class="profile-action-icon" 45 - loading="lazy" 46 - decoding="async" 47 - /> 48 - ) 49 - : ( 50 - <span class="profile-action-icon profile-action-icon--glyph"> 51 - {r.glyph} 52 - </span> 53 - )} 45 + {renderIcon(r.iconKind, r.iconUrl, r.glyph)} 54 46 <span class="profile-action-label"> 55 47 <span class="profile-action-title">{r.title}</span> 56 - <span class="profile-action-sub">{r.subtitle}</span> 57 48 </span> 58 49 </a> 59 50 ))} 60 51 </div> 61 52 ); 62 53 } 54 + 55 + /** 56 + * Pick the right icon renderer in priority order: 57 + * 1. branded inline SVG (matches site palette via currentColor) 58 + * 2. external favicon URL (e.g. alt Bluesky clients) 59 + * 3. text glyph fallback 60 + */ 61 + function renderIcon( 62 + iconKind: ResolvedIconKind | undefined, 63 + iconUrl: string | null, 64 + glyph: string, 65 + ) { 66 + if (iconKind === "bsky") { 67 + return ( 68 + <span class="profile-action-icon profile-action-icon--brand"> 69 + <BskyIcon class="profile-action-icon-svg" /> 70 + </span> 71 + ); 72 + } 73 + if (iconKind === "tangled") { 74 + return ( 75 + <span class="profile-action-icon profile-action-icon--brand"> 76 + <TangledIcon class="profile-action-icon-svg" /> 77 + </span> 78 + ); 79 + } 80 + if (iconKind === "website") { 81 + return ( 82 + <span class="profile-action-icon profile-action-icon--brand"> 83 + <WebsiteIcon class="profile-action-icon-svg" /> 84 + </span> 85 + ); 86 + } 87 + if (iconUrl) { 88 + return ( 89 + <img 90 + src={iconUrl} 91 + alt="" 92 + class="profile-action-icon" 93 + loading="lazy" 94 + decoding="async" 95 + /> 96 + ); 97 + } 98 + return ( 99 + <span class="profile-action-icon profile-action-icon--glyph">{glyph}</span> 100 + ); 101 + }
+28
components/icons/BskyIcon.tsx
··· 1 + interface Props { 2 + /** CSS class on the wrapping <svg>. Color is inherited via currentColor. */ 3 + class?: string; 4 + } 5 + 6 + /** 7 + * Bluesky butterfly mark, simplified vector. Uses `currentColor` so the 8 + * icon picks up whatever foreground color the parent sets — that's how 9 + * we tint it to match the site's blue (#254a9e) rather than rendering 10 + * the bitmap favicon, which is locked to Bluesky's brand sky-blue. 11 + * 12 + * Path is the canonical Bluesky logotype geometry (CC0). 13 + */ 14 + export default function BskyIcon({ class: className }: Props) { 15 + return ( 16 + <svg 17 + viewBox="0 0 600 530" 18 + xmlns="http://www.w3.org/2000/svg" 19 + class={className} 20 + aria-hidden="true" 21 + > 22 + <path 23 + fill="currentColor" 24 + d="M135.72 44.03C202.21 93.94 273.72 195.13 300 249.42c26.28-54.29 97.78-155.49 164.28-205.39C512.26 8.03 590-19.47 590 69.21c0 17.7-10.15 148.79-16.11 170.07-20.7 73.99-96.16 92.87-163.25 81.43 117.27 19.95 147.14 86.07 82.74 152.19-122.27 125.59-175.69-31.51-189.38-71.76-2.51-7.38-3.69-10.83-3.7-7.9-.01-2.93-1.19.52-3.7 7.9-13.69 40.25-67.11 197.35-189.38 71.76-64.4-66.12-34.53-132.24 82.74-152.19-67.09 11.44-142.55-7.44-163.25-81.43C20.15 218 10 86.91 10 69.21 10-19.47 87.74 8.03 135.72 44.03z" 25 + /> 26 + </svg> 27 + ); 28 + }
+27
components/icons/TangledIcon.tsx
··· 1 + interface Props { 2 + class?: string; 3 + } 4 + 5 + /** 6 + * Tangled "dolly" face mark. Path lifted from tangled.org's served 7 + * `dolly.svg` (CC-BY 4.0, Tangled). We use `currentColor` so the icon 8 + * inherits the site's primary blue rather than rendering as flat 9 + * black, matching the rest of the Atmosphere palette. 10 + */ 11 + export default function TangledIcon({ class: className }: Props) { 12 + return ( 13 + <svg 14 + viewBox="0 0 25 25" 15 + xmlns="http://www.w3.org/2000/svg" 16 + class={className} 17 + aria-hidden="true" 18 + > 19 + <g transform="translate(-0.42924038,-0.87777209)"> 20 + <path 21 + fill="currentColor" 22 + d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z" 23 + /> 24 + </g> 25 + </svg> 26 + ); 27 + }
+28
components/icons/WebsiteIcon.tsx
··· 1 + interface Props { 2 + class?: string; 3 + } 4 + 5 + /** 6 + * Generic globe / "website" icon used for the Website link button on 7 + * the public profile. Inline SVG so it inherits `currentColor` and 8 + * matches the site's blue alongside the branded service marks. 9 + */ 10 + export default function WebsiteIcon({ class: className }: Props) { 11 + return ( 12 + <svg 13 + viewBox="0 0 24 24" 14 + xmlns="http://www.w3.org/2000/svg" 15 + class={className} 16 + fill="none" 17 + stroke="currentColor" 18 + stroke-width="1.6" 19 + stroke-linecap="round" 20 + stroke-linejoin="round" 21 + aria-hidden="true" 22 + > 23 + <circle cx="12" cy="12" r="9.5" /> 24 + <ellipse cx="12" cy="12" rx="4" ry="9.5" /> 25 + <line x1="2.5" y1="12" x2="21.5" y2="12" /> 26 + </svg> 27 + ); 28 + }
+1 -1
i18n/messages/en.tsx
··· 367 367 detail: { 368 368 openOn: "Open on", 369 369 lastUpdated: "Last updated", 370 - hostedOn: "Hosted on", 370 + hostedOn: "Account Provider", 371 371 editProfile: "Edit this profile", 372 372 missingProfile: "We couldn't find a profile for that handle.", 373 373 backToExplore: "Back to Explore",
+44
lib/account-providers.ts
··· 1 + /** 2 + * Map a raw PDS URL → a friendly "account provider" name shown on the 3 + * public profile footer. 4 + * 5 + * The PDS host is usually a per-shard fungus name (e.g. 6 + * `shimeji.us-east.host.bsky.network`) which isn't very useful in UI. 7 + * We collapse known umbrella providers to their canonical brand and 8 + * fall back to the bare hostname for anything else. 9 + * 10 + * Add new providers by extending `KNOWN_PROVIDERS` — keep the patterns 11 + * specific enough that we don't accidentally rebrand somebody's 12 + * self-hosted PDS. 13 + */ 14 + 15 + interface ProviderMatcher { 16 + /** Predicate: does this hostname belong to the provider? */ 17 + match: (host: string) => boolean; 18 + /** Friendly display name. */ 19 + name: string; 20 + } 21 + 22 + const KNOWN_PROVIDERS: ProviderMatcher[] = [ 23 + { 24 + name: "Bluesky", 25 + match: (host) => 26 + host === "bsky.network" || 27 + host === "bsky.social" || 28 + host.endsWith(".bsky.network"), 29 + }, 30 + ]; 31 + 32 + export function accountProviderName(pdsUrl: string | null | undefined): string { 33 + if (!pdsUrl) return ""; 34 + let host: string; 35 + try { 36 + host = new URL(pdsUrl).host; 37 + } catch { 38 + return pdsUrl; 39 + } 40 + for (const p of KNOWN_PROVIDERS) { 41 + if (p.match(host)) return p.name; 42 + } 43 + return host; 44 + }
+23 -1
lib/atmosphere-links.ts
··· 132 132 custom: string; 133 133 } 134 134 135 + /** 136 + * A semantic icon "kind" — when set, `ProfileLinks.tsx` renders the 137 + * matching inline-SVG component (which inherits the site's blue via 138 + * currentColor) instead of falling back to the favicon `iconUrl`. 139 + * 140 + * - `bsky` — inline Bluesky butterfly (only for the canonical 141 + * bluesky client; alt clients keep their favicon) 142 + * - `tangled` — inline Tangled "dolly" mark 143 + * - `website` — inline globe 144 + */ 145 + export type ResolvedIconKind = "bsky" | "tangled" | "website"; 146 + 135 147 export interface ResolvedLink { 136 148 /** Display title for the button. */ 137 149 title: string; 138 - /** Subtitle (host + path of the URL). */ 150 + /** Subtitle (host + path of the URL). Buttons opt out of rendering 151 + * this in `ProfileLinks` for atmosphere kinds + website; it's 152 + * preserved here for callers that may want it. */ 139 153 subtitle: string; 140 154 /** Icon URL, when available. */ 141 155 iconUrl: string | null; ··· 143 157 glyph: string; 144 158 /** The final href the user navigates to. */ 145 159 href: string; 160 + /** Branded inline-SVG to use instead of `iconUrl`. */ 161 + iconKind?: ResolvedIconKind; 146 162 } 147 163 148 164 function trimUrlForDisplay(url: string): string { ··· 181 197 iconUrl: client.iconUrl, 182 198 glyph: "B", 183 199 href, 200 + // Only the canonical Bluesky client gets the branded inline SVG; 201 + // alt clients (Blacksky, Witchsky, etc.) keep their favicons so 202 + // users can tell them apart at a glance. 203 + iconKind: client.id === "bluesky" ? "bsky" : undefined, 184 204 }; 185 205 } 186 206 ··· 193 213 iconUrl: svc.iconUrl, 194 214 glyph: svc.name.slice(0, 1), 195 215 href, 216 + iconKind: kind === "tangled" ? "tangled" : undefined, 196 217 }; 197 218 } 198 219 ··· 204 225 iconUrl: null, 205 226 glyph: "↗", 206 227 href: entry.url, 228 + iconKind: "website", 207 229 }; 208 230 } 209 231
+6 -8
routes/explore/[handle].tsx
··· 11 11 getProfileByHandle, 12 12 type ProfileRow, 13 13 } from "../../lib/registry.ts"; 14 + import { accountProviderName } from "../../lib/account-providers.ts"; 14 15 15 16 export const handler = define.handlers({ 16 17 async GET(ctx) { ··· 67 68 publicProfileHandle: ownerHandle, 68 69 }; 69 70 const lastUpdated = new Date(profile.indexedAt).toISOString().slice(0, 10); 70 - const pdsHost = (() => { 71 - try { 72 - return new URL(profile.pdsUrl).host; 73 - } catch { 74 - return profile.pdsUrl; 75 - } 76 - })(); 71 + /** PDS hosts are usually per-shard (e.g. shimeji.us-east.host.bsky.network) 72 + * which isn't useful in UI. Collapse known umbrella PDSes to their 73 + * brand name (Bluesky, etc.) and fall back to the bare host. */ 74 + const providerName = accountProviderName(profile.pdsUrl); 77 75 return ( 78 76 <div id="page-top"> 79 77 <GlassClouds /> ··· 104 102 {t.detail.lastUpdated}: <strong>{lastUpdated}</strong> 105 103 </span> 106 104 <span> 107 - {t.detail.hostedOn}: <strong>{pdsHost}</strong> 105 + {t.detail.hostedOn}: <strong>{providerName}</strong> 108 106 </span> 109 107 </div> 110 108 </div>