diff --git a/src/components/actor-app.tsx b/src/components/actor-app.tsx index 28e9aa3..91467aa 100644 --- a/src/components/actor-app.tsx +++ b/src/components/actor-app.tsx @@ -6,6 +6,7 @@ import { HauntSigil } from "~/components/haunt-sigil"; import { IdentityCard } from "~/components/identity-card"; import { MasonryGrid } from "~/components/masonry-grid"; import { ProfileSections } from "~/components/profile-sections"; +import { ProfileSigilArtifact } from "~/components/profile-sigil-artifact"; import { resolveMiniDoc } from "~/lib/atproto/mini-doc"; import { getBlueskyProfile } from "~/lib/providers/bluesky"; import { searchBlueskyActors } from "~/lib/providers/bluesky-search"; @@ -123,17 +124,19 @@ export function ActorApp() { > - + {profile ? ( ) : ( )} + + @@ -44,12 +46,6 @@ export function BlueskyProfile({ did, handle, profile }: BlueskyProfileProps) {

)} - - {profile.description && ( diff --git a/src/components/grain-photos.tsx b/src/components/grain-photos.tsx index 815b76b..72aadc3 100644 --- a/src/components/grain-photos.tsx +++ b/src/components/grain-photos.tsx @@ -1,35 +1,16 @@ import type { GrainPhoto } from "~/lib/providers/grain-photo"; -import { ProfileSection } from "~/components/profile-section"; -type GrainPhotosProps = { - photos: GrainPhoto[]; -}; - -export function GrainPhotos({ photos }: GrainPhotosProps) { +import { ProfileArtifact } from "~/components/profile-artifact"; +export function GrainPhotoArtifact({ photo }: { photo: GrainPhoto }) { return ( - -
- {photos.map((photo) => - photo.postUrl ? ( - - - - ) : ( - - ), - )} -
-
+ + {photo.postUrl ? ( + + + + ) : ( + + )} + ); } diff --git a/src/components/masonry-grid.tsx b/src/components/masonry-grid.tsx index 9765697..6f78df4 100644 --- a/src/components/masonry-grid.tsx +++ b/src/components/masonry-grid.tsx @@ -1,13 +1,16 @@ import type { ReactNode } from "react"; import { useLayoutEffect, useRef } from "react"; +import { hashString } from "~/lib/did-random"; + const MASONRY_MEDIA_QUERY = "(min-width: 40rem)"; type MasonryGridProps = { children: ReactNode; + seed: string; }; -export function MasonryGrid({ children }: MasonryGridProps) { +export function MasonryGrid({ children, seed }: MasonryGridProps) { const gridRef = useRef(null); useLayoutEffect(() => { @@ -31,7 +34,7 @@ export function MasonryGrid({ children }: MasonryGridProps) { if (!mediaQuery.matches) { gridElement.classList.remove("profile-grid-masonry"); - cards.forEach(clearCardPlacement); + cards.forEach(clearCardLayout); return; } @@ -48,13 +51,15 @@ export function MasonryGrid({ children }: MasonryGridProps) { const columnHeights = Array(columnCount).fill(0); - cards.forEach((card) => { + cards.forEach((card, index) => { const columnIndex = getShortestColumn(columnHeights); - const rowSpan = getRowSpan(card, rowHeight, rowGap); card.style.gridColumnStart = String(columnIndex + 1); + const rowSpan = getRowSpan(card, rowHeight, rowGap); + card.style.gridRowStart = String(columnHeights[columnIndex]! + 1); card.style.gridRowEnd = `span ${rowSpan}`; + setArtifactOffset(card, seed, index); columnHeights[columnIndex]! += rowSpan; }); } @@ -73,7 +78,7 @@ export function MasonryGrid({ children }: MasonryGridProps) { mutationObserver.disconnect(); mediaQuery.removeEventListener("change", scheduleLayout); }; - }, []); + }, [seed]); return (
@@ -82,10 +87,21 @@ export function MasonryGrid({ children }: MasonryGridProps) { ); } -function clearCardPlacement(card: HTMLElement) { +function clearCardLayout(card: HTMLElement) { card.style.removeProperty("grid-column-start"); card.style.removeProperty("grid-row-start"); card.style.removeProperty("grid-row-end"); + card.style.removeProperty("--artifact-offset-x"); + card.style.removeProperty("--artifact-offset-y"); +} + +function setArtifactOffset(card: HTMLElement, seed: string, index: number) { + const hash = hashString(`${seed}:artifact:${index}`); + const horizontalOffset = (hash % 17) - 8; + const verticalOffset = ((hash >>> 5) % 13) - 6; + + card.style.setProperty("--artifact-offset-x", `${horizontalOffset}px`); + card.style.setProperty("--artifact-offset-y", `${verticalOffset}px`); } function getColumnCount(gridTemplateColumns: string): number { diff --git a/src/components/profile-artifact.tsx b/src/components/profile-artifact.tsx new file mode 100644 index 0000000..61b66a1 --- /dev/null +++ b/src/components/profile-artifact.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from "react"; + +type ProfileArtifactProps = { + children: ReactNode; + kind?: "sigil"; + source?: { + href: string; + name: string; + }; +}; + +export function ProfileArtifact({ + children, + kind, + source, +}: ProfileArtifactProps) { + return ( + + ); +} diff --git a/src/components/profile-section.tsx b/src/components/profile-section.tsx deleted file mode 100644 index 0a5b803..0000000 --- a/src/components/profile-section.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { ReactNode } from "react"; - -type ProfileSectionProps = { - children: ReactNode; - source?: { - href: string; - name: string; - }; - title: string; -}; - -export function ProfileSection({ - title, - source, - children, -}: ProfileSectionProps) { - return ( -
- {source ? ( -
-

{title}

-
- ) : ( -

{title}

- )} - {children} - {source && ( - - )} -
- ); -} diff --git a/src/components/profile-sections.tsx b/src/components/profile-sections.tsx index ac2dbe8..77c6ed5 100644 --- a/src/components/profile-sections.tsx +++ b/src/components/profile-sections.tsx @@ -1,11 +1,10 @@ -import type { ReactNode } from "react"; import { useEffect, useState } from "react"; -import { GrainPhotos } from "~/components/grain-photos"; -import { ProfileSection } from "~/components/profile-section"; -import { RockskyScrobbleList } from "~/components/rocksky-scrobble-list"; -import { StandardDocuments } from "~/components/standard-documents"; -import { TangledRepoList } from "~/components/tangled-repo-list"; +import { GrainPhotoArtifact } from "~/components/grain-photos"; +import { ProfileArtifact } from "~/components/profile-artifact"; +import { RockskyScrobble } from "~/components/rocksky-scrobble-list"; +import { StandardDocumentArtifact } from "~/components/standard-documents"; +import { TangledRepoArtifact } from "~/components/tangled-repo-list"; import { getDidSectionOrder } from "~/lib/layout/did-section-order"; import { getRecentGrainPhotos } from "~/lib/providers/grain-photo"; import { getRecentRocksky } from "~/lib/providers/rocksky"; @@ -53,33 +52,41 @@ export function ProfileSections({ did, handle, pds }: ProfileSectionsProps) { if (!data) { return ( - + +

Gathering signals

Searching the Atmosphere…

-
+ ); } - const sections: ReactNode[] = []; - - if (data.scrobbles.length > 0) { - sections.push( - , - ); - } - - if (data.tangledRepos.length > 0) { - sections.push(); - } - - if (data.documents.length > 0) { - sections.push( - , - ); - } - - if (data.photos.length > 0) { - sections.push(); - } - - return <>{getDidSectionOrder(did, sections)}; + const artifacts = [ + ...data.scrobbles.map((scrobble) => ( + + )), + ...data.tangledRepos.map((repo) => ( + + )), + ...data.documents.map((document) => ( + + )), + ...data.photos.map((photo) => ( + + )), + ]; + + return <>{getDidSectionOrder(did, artifacts)}; } diff --git a/src/components/profile-sigil-artifact.tsx b/src/components/profile-sigil-artifact.tsx new file mode 100644 index 0000000..f8b796c --- /dev/null +++ b/src/components/profile-sigil-artifact.tsx @@ -0,0 +1,14 @@ +import { HauntSigil } from "~/components/haunt-sigil"; +import { ProfileArtifact } from "~/components/profile-artifact"; + +export function ProfileSigilArtifact({ did }: { did: string }) { + return ( + + + + ); +} diff --git a/src/components/rocksky-scrobble-list.tsx b/src/components/rocksky-scrobble-list.tsx index 20077f8..fe40e8f 100644 --- a/src/components/rocksky-scrobble-list.tsx +++ b/src/components/rocksky-scrobble-list.tsx @@ -1,38 +1,11 @@ import type { RockskyScrobbleRecord } from "~/lib/providers/rocksky"; -import { ProfileSection } from "~/components/profile-section"; - -type RockskyScrobbleListProps = { - scrobbles: RockskyScrobbleRecord[]; -}; +import { ProfileArtifact } from "~/components/profile-artifact"; type RockskyScrobbleProps = Pick< RockskyScrobbleRecord, "album" | "albumArtUrl" | "artist" | "createdAt" | "spotifyLink" | "title" >; -export function RockskyScrobbleList({ scrobbles }: RockskyScrobbleListProps) { - return ( - -
- {scrobbles.map((scrobble) => ( - - ))} -
-
- ); -} - function formatRelativeTime(createdAt: string): string { const elapsedMinutes = Math.round( (Date.now() - new Date(createdAt).getTime()) / 60000, @@ -47,7 +20,7 @@ function formatRelativeTime(createdAt: string): string { return `${Math.round(elapsedHours / 24)} days ago`; } -function RockskyScrobble({ +export function RockskyScrobble({ title, artist, album, @@ -56,34 +29,33 @@ function RockskyScrobble({ albumArtUrl, }: RockskyScrobbleProps) { return ( - - {albumArtUrl ? ( - {`${album} - ) : ( -
- )} - -
-

{title}

-

- {artist} · {album} -

-
- - - {formatRelativeTime(createdAt)} - -
+ + + {albumArtUrl ? ( + {`${album} + ) : ( +
+ )} + +
+

{title}

+

+ {artist} · {album} +

+
+
+

{formatRelativeTime(createdAt)}

+ ); } diff --git a/src/components/standard-documents.tsx b/src/components/standard-documents.tsx index c1b6d66..307622d 100644 --- a/src/components/standard-documents.tsx +++ b/src/components/standard-documents.tsx @@ -1,51 +1,42 @@ import type { StandardDocument } from "~/lib/providers/standard-site"; -import { ProfileSection } from "~/components/profile-section"; -type StandardDocumentsProps = { - documents: StandardDocument[]; -}; - -export function StandardDocuments({ documents }: StandardDocumentsProps) { +import { ProfileArtifact } from "~/components/profile-artifact"; +export function StandardDocumentArtifact({ + document, +}: { + document: StandardDocument; +}) { return ( - -
- {documents.map((document) => ( -
+ {document.link ? ( + -

- {document.link ? ( - - {document.title} - - ) : ( - document.title - )} -

+ {document.title} + + ) : ( + document.title + )} + - {document.description && ( -

{document.description}

- )} + {document.description && ( +

{document.description}

+ )} -
- {document.publishedAt && ( - - )} -
-
- ))} -
-
+ {document.publishedAt && ( + + )} + ); } diff --git a/src/components/tangled-repo-list.tsx b/src/components/tangled-repo-list.tsx index 66a8dec..40e57ea 100644 --- a/src/components/tangled-repo-list.tsx +++ b/src/components/tangled-repo-list.tsx @@ -1,46 +1,30 @@ import type { ReactNode } from "react"; import type { TangledRepo } from "~/lib/providers/tangled"; -import { ProfileSection } from "~/components/profile-section"; +import { ProfileArtifact } from "~/components/profile-artifact"; -type TangledRepoListProps = { - repos: TangledRepo[]; -}; - -export function TangledRepoList({ repos }: TangledRepoListProps) { +export function TangledRepoArtifact({ repo }: { repo: TangledRepo }) { return ( - -
- {repos.map((repo) => ( - - ))} + {repo.name} + + + + ⭐ {repo.stars} +
-
+

+ {renderMarkdownLinks(repo.description ?? "No description")} +

+ ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index 414b53f..b78cffc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -299,6 +299,13 @@ body { grid-auto-rows: 0.5rem; } +.profile-grid-masonry > * { + transform: translate( + var(--artifact-offset-x, 0), + var(--artifact-offset-y, 0) + ); +} + .profile-card { background-color: var(--profile-card); border-color: color-mix(in srgb, var(--profile-border) 58%, transparent); @@ -338,20 +345,20 @@ body { } .profile-identity-header { - --profile-sigil-size: 2.75rem; - display: grid; - grid-template-columns: 4rem minmax(0, 1fr) var(--profile-sigil-size); + grid-template-columns: 4rem minmax(0, 1fr); align-items: start; gap: 0.875rem; } -.profile-sigil { - height: var(--profile-sigil-size); - justify-self: end; - margin-top: 0.125rem; - opacity: 0.72; - width: var(--profile-sigil-size); +.profile-artifact--sigil { + display: grid; + place-items: center; +} + +.profile-sigil-artifact { + height: clamp(8rem, 18vw, 10rem); + width: clamp(8rem, 18vw, 10rem); } .profile-name { @@ -361,12 +368,6 @@ body { line-height: 1.05; } -.profile-section-title { - font-size: 1rem; - font-weight: 650; - line-height: 1.3; -} - .profile-source-link { font-size: 0.75rem; line-height: 1.4; @@ -377,6 +378,13 @@ body { text-underline-offset: 0.18em; } +.profile-artifact-source { + display: flex; + justify-content: flex-end; + margin-top: 1rem; + padding-top: 0.75rem; +} + .profile-source-link:hover, .profile-source-link:focus-visible { opacity: 0.82; @@ -411,15 +419,6 @@ body { opacity: 0.62; } -@container (max-width: 21rem) { - .profile-identity-header { - --profile-sigil-size: 2.25rem; - - gap: 0.75rem; - } -} - - @keyframes atmosphere-drift { to { background-position: 100% 100%;