this repo has no description
10
fork

Configure Feed

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

Dev resources: account rail, icon deep link, and cache-busted icon URLs

- Show AccountMenu on /developer-resources with buildAccountMenuProps
- Add #project-icons anchor and support ?icon=&variant= in SvgIconDownloads
- Add manage form link to developer resources; PUT profile returns icon refs
- Version icon proxy URLs with ?v=cid to avoid stale browser/CDN caches
- US spelling and OG alt copy tweaks

Made-with: Cursor

+114 -22
+9 -2
assets/styles.css
··· 3592 3592 color: rgba(255, 255, 255, 0.5); 3593 3593 } 3594 3594 3595 - /* Two parallel slots — colour and B/W — share the same access gate 3595 + /* Two parallel slots — color and B/W — share the same access gate 3596 3596 and sanitiser, so we render them side-by-side. They wrap to a single 3597 3597 column on narrow viewports so the upload buttons stay reachable. */ 3598 3598 .profile-form-icon-grid { ··· 3640 3640 linear-gradient(-45deg, rgba(255, 255, 255, 0.06) 25%, transparent 25%), 3641 3641 linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.06) 75%), 3642 3642 linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.06) 75%); 3643 + } 3644 + .profile-form-icon-resource-actions { 3645 + margin-top: 0.75rem; 3646 + } 3647 + .profile-form-icon-resource-link { 3648 + text-decoration: none; 3649 + color: inherit; 3643 3650 } 3644 3651 3645 3652 /* ------------------------------------------------------------------ * ··· 4932 4939 min-height: 2.4rem; 4933 4940 } 4934 4941 4935 - /* Colour / B/W toggle. Sits between the search input and the ZIP 4942 + /* Color / B/W toggle. Sits between the search input and the ZIP 4936 4943 button so the toolbar reads search → variant → bulk-download. */ 4937 4944 .svg-download-tabs { 4938 4945 display: inline-flex;
+1 -1
components/DeveloperResources.tsx
··· 39 39 </div> 40 40 </section> 41 41 42 - <section class="section-sm"> 42 + <section class="section-sm" id="project-icons"> 43 43 <div class="container-narrow text-center"> 44 44 <h2 class="text-subsection">{t.developerResources.lottieHeading}</h2> 45 45 <div class="divider" />
+3 -2
i18n/messages/en.tsx
··· 310 310 error: "Could not load icons: {error}", 311 311 iconAlt: "{name} SVG icon", 312 312 variantToggleLabel: "Icon variant", 313 - variantColor: "Colour", 313 + variantColor: "Color", 314 314 variantBw: "B/W", 315 315 }, 316 316 schemaHeading: "Profile schema", ··· 670 670 upload: "Upload SVG", 671 671 replace: "Replace SVG", 672 672 remove: "Remove SVG", 673 - colorLabel: "Colour", 673 + colorLabel: "Color", 674 674 colorHint: "Your primary mark, used by default in badges and listings.", 675 675 bwLabel: "Black & white", 676 676 bwHint: ··· 678 678 bwUpload: "Upload B/W SVG", 679 679 bwReplace: "Replace B/W SVG", 680 680 bwRemove: "Remove B/W SVG", 681 + viewOnDeveloperResources: "View on developer resources", 681 682 invalidType: "Icon must be an SVG (image/svg+xml).", 682 683 tooLarge: "Icon must be 200KB or smaller.", 683 684 gate: {
+55 -4
islands/CreateProfileForm.tsx
··· 69 69 */ 70 70 const APPEAL_EMAIL = "contact@atmosphereaccount.com"; 71 71 72 + function iconPreviewRoute( 73 + did: string, 74 + variant: "color" | "bw", 75 + ref: string, 76 + ): string { 77 + const path = variant === "bw" ? "icon-bw" : "icon"; 78 + return `/api/registry/${path}/${encodeURIComponent(did)}?v=${ 79 + encodeURIComponent(ref) 80 + }`; 81 + } 82 + 83 + function developerResourcesIconHref( 84 + handle: string, 85 + variant: "color" | "bw", 86 + ): string { 87 + const params = new URLSearchParams({ 88 + icon: handle, 89 + variant, 90 + }); 91 + return `/developer-resources?${params.toString()}#project-icons`; 92 + } 93 + 72 94 interface Props { 73 95 did: string; 74 96 handle: string; ··· 339 361 */ 340 362 const iconKeep = useSignal<BlobRefShape | null>(null); 341 363 const iconPreviewUrl = useSignal<string | null>( 342 - initial?.icon ? `/api/registry/icon/${encodeURIComponent(did)}` : null, 364 + initial?.icon ? iconPreviewRoute(did, "color", initial.icon.ref) : null, 343 365 ); 344 366 const iconFile = useSignal<File | null>(null); 345 367 const iconRemoved = useSignal(false); 346 368 347 369 const iconBwKeep = useSignal<BlobRefShape | null>(null); 348 370 const iconBwPreviewUrl = useSignal<string | null>( 349 - initial?.iconBw ? `/api/registry/icon-bw/${encodeURIComponent(did)}` : null, 371 + initial?.iconBw ? iconPreviewRoute(did, "bw", initial.iconBw.ref) : null, 350 372 ); 351 373 const iconBwFile = useSignal<File | null>(null); 352 374 const iconBwRemoved = useSignal(false); ··· 763 785 const text = await res.text(); 764 786 throw new Error(text || `HTTP ${res.status}`); 765 787 } 788 + const saved = await res.json() as { 789 + icon?: BlobRefShape | null; 790 + iconBw?: BlobRefShape | null; 791 + }; 792 + iconKeep.value = saved.icon ?? null; 793 + iconPreviewUrl.value = saved.icon 794 + ? iconPreviewRoute(did, "color", saved.icon.ref.$link) 795 + : null; 796 + iconFile.value = null; 797 + iconRemoved.value = false; 798 + iconBwKeep.value = saved.iconBw ?? null; 799 + iconBwPreviewUrl.value = saved.iconBw 800 + ? iconPreviewRoute(did, "bw", saved.iconBw.ref.$link) 801 + : null; 802 + iconBwFile.value = null; 803 + iconBwRemoved.value = false; 766 804 published.value = true; 767 805 message.value = { kind: "ok", text: tManage.savedToast }; 768 806 } catch (err) { ··· 1229 1267 </p> 1230 1268 )} 1231 1269 1232 - {/* ---- Two slots: colour + optional B/W companion ---- */} 1270 + {/* ---- Two slots: color + optional B/W companion ---- */} 1233 1271 { 1234 1272 /* 1235 - Colour and B/W share the same access gate, sanitiser, and 1273 + Color and B/W share the same access gate, sanitiser, and 1236 1274 200KB cap — we just persist them to parallel `icon_*` / 1237 1275 `icon_bw_*` columns and surface both on the developer 1238 1276 downloads UI. ··· 1269 1307 /> 1270 1308 </div> 1271 1309 <p class="profile-form-hint">{tIcon.hint}</p> 1310 + {(iconKeep.value || iconBwKeep.value) && ( 1311 + <div class="profile-form-icon-resource-actions"> 1312 + <a 1313 + href={developerResourcesIconHref( 1314 + handle, 1315 + iconKeep.value ? "color" : "bw", 1316 + )} 1317 + class="profile-form-button-secondary profile-form-icon-resource-link" 1318 + > 1319 + {tIcon.viewOnDeveloperResources} 1320 + </a> 1321 + </div> 1322 + )} 1272 1323 </div> 1273 1324 </div> 1274 1325
+8
islands/SvgIconDownloads.tsx
··· 27 27 const error = useSignal<string | null>(null); 28 28 29 29 useEffect(() => { 30 + const params = new URLSearchParams(globalThis.location?.search ?? ""); 31 + const initialQuery = params.get("icon") ?? params.get("q"); 32 + const initialVariant = params.get("variant"); 33 + if (initialQuery) query.value = initialQuery.replace(/^@/, ""); 34 + if (initialVariant === "bw" || initialVariant === "color") { 35 + tab.value = initialVariant; 36 + } 37 + 30 38 let cancelled = false; 31 39 async function loadIcons() { 32 40 loading.value = true;
+1 -1
lexicons/com/atmosphereaccount/registry/profile.json
··· 63 63 "type": "blob", 64 64 "accept": ["image/svg+xml"], 65 65 "maxSize": 200000, 66 - "description": "Optional black & white companion to `icon`. Same audience and sanitisation rules — published alongside the colour mark for monochrome contexts (light/dark badges, sign-in chrome, print)." 66 + "description": "Optional black & white companion to `icon`. Same audience and sanitisation rules — published alongside the color mark for monochrome contexts (light/dark badges, sign-in chrome, print)." 67 67 }, 68 68 "screenshots": { 69 69 "type": "array",
+6 -2
lib/public-profile.ts
··· 67 67 const iconUrl = profile.iconCid && 68 68 profile.iconStatus === "approved" && 69 69 profile.iconAccessStatus === "granted" 70 - ? `${origin}/api/registry/icon/${encodeURIComponent(profile.did)}` 70 + ? `${origin}/api/registry/icon/${encodeURIComponent(profile.did)}?v=${ 71 + encodeURIComponent(profile.iconCid) 72 + }` 71 73 : null; 72 74 const iconBwUrl = profile.iconBwCid && 73 75 profile.iconBwStatus === "approved" && 74 76 profile.iconAccessStatus === "granted" 75 - ? `${origin}/api/registry/icon-bw/${encodeURIComponent(profile.did)}` 77 + ? `${origin}/api/registry/icon-bw/${encodeURIComponent(profile.did)}?v=${ 78 + encodeURIComponent(profile.iconBwCid) 79 + }` 76 80 : null; 77 81 const screenshotUrls = profile.screenshots.map((_, i) => 78 82 `${origin}/api/registry/screenshot/${encodeURIComponent(profile.did)}/${i}`
+3 -3
lib/registry.ts
··· 80 80 iconReviewedAt: number | null; 81 81 iconRejectedReason: string | null; 82 82 /** Optional black-and-white companion to `iconCid`. Same access gate 83 - * and per-icon approval lifecycle as the colour icon — surfaced on 83 + * and per-icon approval lifecycle as the color icon — surfaced on 84 84 * the developer downloads UI alongside (or instead of) `iconCid`. */ 85 85 iconBwCid: string | null; 86 86 iconBwMime: string | null; ··· 435 435 END, 436 436 /** 437 437 * Black-and-white companion icon — same per-project gate 438 - * and lifecycle as the colour icon. Tracked independently so 438 + * and lifecycle as the color icon. Tracked independently so 439 439 * a project can swap one variant without revoking the other. 440 440 */ 441 441 icon_bw_cid=excluded.icon_bw_cid, ··· 1081 1081 1082 1082 /** 1083 1083 * Public projection of every approved developer SVG icon. A project 1084 - * appears once it has at least one approved variant (colour or B/W); 1084 + * appears once it has at least one approved variant (color or B/W); 1085 1085 * the downloads UI decides which buttons to render per row based on 1086 1086 * `iconCid` / `iconBwCid` presence. 1087 1087 */
+3 -2
lib/svg-icon-downloads.ts
··· 11 11 did: string; 12 12 handle: string; 13 13 name: string; 14 - /** Colour variant. `null` when the project has only published a B/W icon. */ 14 + /** Color variant. `null` when the project has only published a B/W icon. */ 15 15 color: PublicSvgIconVariant | null; 16 16 /** Optional black-and-white companion. `null` when not uploaded / approved. */ 17 17 bw: PublicSvgIconVariant | null; ··· 50 50 variant: IconVariant, 51 51 origin: string, 52 52 ): PublicSvgIconVariant { 53 + const cid = variant === "bw" ? profile.iconBwCid : profile.iconCid; 53 54 return { 54 55 iconUrl: `${origin}/api/registry/${variantUrlPath(variant)}/${ 55 56 encodeURIComponent(profile.did) 56 - }`, 57 + }?v=${encodeURIComponent(cid ?? "")}`, 57 58 downloadFilename: svgIconDownloadFilename(profile, variant), 58 59 }; 59 60 }
+1 -1
routes/_app.tsx
··· 260 260 ? "/og-explore.png" 261 261 : "/og-hero.png"; 262 262 const socialImageAlt = url.pathname.startsWith("/developer-resources") 263 - ? "Atmosphere developer resources: sign-in badges, project icons, and the registry API." 263 + ? "Atmosphere developer resources: tools to make the Atmosphere easier to understand." 264 264 : url.pathname.startsWith("/explore") 265 265 ? "Explore the Atmosphere registry: apps, profiles, reviews, and updates." 266 266 : t.meta.ogImageAlt;
+4 -2
routes/api/registry/profile.ts
··· 232 232 } 233 233 234 234 /** 235 - * Developer-facing SVG icons (colour + optional B/W companion). 235 + * Developer-facing SVG icons (color + optional B/W companion). 236 236 * Two gates apply identically to both variants: 237 237 * 238 238 * 1. Per-project verification (`icon_access_status === 'granted'`). ··· 268 268 } 269 269 270 270 /** 271 - * Process one icon-variant upload. Centralised so the colour and 271 + * Process one icon-variant upload. Centralised so the color and 272 272 * B/W slots stay 1:1 — same MIME check, size cap, sanitiser, and 273 273 * PDS upload path. 274 274 * ··· 479 479 ok: true, 480 480 uri: result.uri, 481 481 cid: result.cid, 482 + icon: validation.value.icon ?? null, 483 + iconBw: validation.value.iconBw ?? null, 482 484 }), 483 485 { status: 200, headers: { "content-type": "application/json" } }, 484 486 );
+20 -2
routes/developer-resources.tsx
··· 2 2 import Nav from "../components/Nav.tsx"; 3 3 import DeveloperResources from "../components/DeveloperResources.tsx"; 4 4 import Footer from "../components/Footer.tsx"; 5 + import { buildAccountMenuProps } from "../lib/account-menu-props.ts"; 6 + import { getProfileByDid } from "../lib/registry.ts"; 5 7 6 - export default define.page(function DeveloperResourcesPage() { 8 + function DeveloperResourcesPage( 9 + { account }: { account: ReturnType<typeof buildAccountMenuProps> }, 10 + ) { 7 11 return ( 8 12 <div id="page-top"> 9 13 <div class="content-layer"> 10 - <Nav disableScrollEffects /> 14 + <Nav account={account} disableScrollEffects /> 11 15 <section style={{ paddingTop: "8rem" }}> 12 16 <DeveloperResources /> 13 17 </section> ··· 15 19 </div> 16 20 </div> 17 21 ); 22 + } 23 + 24 + export const handler = define.handlers({ 25 + async GET(ctx) { 26 + const user = ctx.state.user; 27 + const ownerProfile = user 28 + ? await getProfileByDid(user.did).catch(() => null) 29 + : null; 30 + return ctx.render( 31 + <DeveloperResourcesPage 32 + account={buildAccountMenuProps(ctx.state, ownerProfile?.handle ?? null)} 33 + />, 34 + ); 35 + }, 18 36 });