From c7ea2b810291641a6b2b02499cdc0f18b45a715a Mon Sep 17 00:00:00 2001 From: Brittany Ellich Date: Tue, 02 Jun 2026 05:51:11 +0000 Subject: [PATCH] docs: spec for profile social logo links Co-Authored-By: Claude Opus 4.8 --- docs/superpowers/specs/2026-06-01-profile-social-logo-links-design.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file(s) changed, 132 insertion(s)(+), 0 deletion(s)(-) diff --git a/docs/superpowers/specs/2026-06-01-profile-social-logo-links-design.md b/docs/superpowers/specs/2026-06-01-profile-social-logo-links-design.md new file mode 100644 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-profile-social-logo-links-design.md @@ -0,0 +1,132 @@ +# Profile social logo links — design + +## Goal + +Surface a row of monochrome logo links on atmo.quest profiles for the external +services a user is present on: + +- **Bluesky** — when the user has an `app.bsky.actor.profile/self` record. + Links to `https://bsky.app/profile/`. +- **Tangled** — when the user has an `sh.tangled.actor.profile/self` record in + their repo. Links to `https://tangled.org/`. +- **sifa.id** — when the user has an `id.sifa.profile.self/self` record. Links + to `https://sifa.id/p/`. + +Shown on **both** the signed-in user's own profile (`/profile`, whoami) and on +connection profiles viewed via `/connections`. + +## Detection layer (`internal/profile`) + +New types and functions: + +```go +// SocialLink is one external-service logo link. +type SocialLink struct { + Service string // "bluesky" | "tangled" | "sifa" — drives which glyph renders + Label string // "Bluesky" | "Tangled" | "sifa.id" — used for title/aria-label + URL string +} + +// SocialLinks probes the user's PDS and assembles the social links to show. +// Soft-fails per service: a getRecord error is treated as "absent", never +// surfaced to the caller, so a slow/erroring PDS never blocks the page. +func SocialLinks(ctx context.Context, pdsHost string, did syntax.DID, handle string, hasBsky bool) []SocialLink + +// RecordExists reports whether did has a record at collection/rkey on pdsHost. +// ErrNotFound -> (false, nil). Other errors bubble up. +func RecordExists(ctx context.Context, pdsHost string, did syntax.DID, collection, rkey string) (bool, error) +``` + +Detection rules inside `SocialLinks`: + +| Service | Condition | URL | +|---------|-----------|-----| +| Bluesky | `hasBsky` true | `https://bsky.app/profile/`, falling back to `` when handle is empty | +| Tangled | `RecordExists(sh.tangled.actor.profile, self)` AND handle non-empty | `https://tangled.org/` | +| sifa.id | `RecordExists(id.sifa.profile.self, self)` AND handle non-empty | `https://sifa.id/p/` | + +Tangled and sifa.id URLs are handle-based with no DID form, so both are skipped +when the handle is empty even if the record exists. Bluesky tolerates a missing +handle by falling back to the DID (bsky.app resolves both). + +`RecordExists` is a thin wrapper over the existing `getRecord` plumbing in +`internal/profile/profile.go` (`fetchRecord` + `isRecordMissing`). It adds two +extra `getRecord` calls per profile render (tangled + sifa); both are soft-failed. + +Rejected alternative: inlining the `getRecord` calls in each handler. A shared +`internal/profile` function is the natural fit — both handlers already call +`FetchBluesky`/`FetchQuest` there — and it is unit-testable in isolation. + +## Handler + view wiring + +**Own profile** (`features/profile/handlers.go`, `Handlers.Profile`): after +fetching `bsky`/`quest`, resolve the handle via +`users.NameAndHandle(ctx, h.DB, did)` (the users row is touched with the handle +at auth time), then call `profile.SocialLinks(ctx, pds, did, handle, bsky != nil)` +and attach the result to the view. Local users (no PDS, `IsLocal`) are skipped +entirely — no social links. + +**Connection profile** (`features/connections/handlers.go`, `Handlers.View`): +the handle is already resolved into `view.Handle` and `bsky` is already fetched, +so call `profile.SocialLinks(ctx, targetPDS, target, view.Handle, bsky != nil)` +after the existing fetches and attach to the view. + +Both view structs gain a field backed by a small view-layer mirror type +(mirroring the existing `ProfileLink` pattern): + +```go +// in features/profile/pages and features/connections/pages +type SocialLink struct { + Service string + Label string + URL string +} + +// added to each ProfileView +SocialLinks []SocialLink +``` + +## Rendering (the logo row) + +A new templ block renders only when `len(SocialLinks) > 0`: + +- **Own profile** (`features/profile/pages/profile.templ`): right after the + `profile-status-pills` block and before `profile-details`. +- **Connection profile** (`features/connections/pages/profile.templ`): right + after the `pv-badges` row. + +Each link: + +```html + +``` + +Glyphs are inline templ SVG components using `fill="currentColor"` so the +terminal palette controls color via CSS (single accent color, hover brightens). +New `.social-logos` (the row) and `.social-logo` (each icon button) rules go in +`web/resources/static/css/terminal.css`, matching the existing terminal/retro +aesthetic. + +**Logo assets:** the official Bluesky butterfly SVG path is used for Bluesky. +The Tangled and sifa.id marks are sourced from their sites/repos during +implementation; if a clean monochrome mark is not readily available for one, a +simple lettermark glyph (stylized "t" / "s") is used as a fallback rather than +blocking. Rendered result is shown for confirmation before finalizing. + +## Testing + +- Unit tests for `SocialLinks` / `RecordExists` covering: bsky present/absent, + tangled & sifa record present/absent, and the no-handle case (tangled/sifa + suppressed, bluesky falls back to DID). +- Regenerate both templ views (`templ generate`) and build the app to confirm + rendering. + +## Out of scope + +- No editing UI: these links are auto-detected from PDS records, not entered by + the user. +- No new services beyond the three named above. +- Local (non-ATProto) accounts show no social links. -- tangled.sh