grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

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

various style changes, add share gallery button, move alt dialog to upload page

+204 -126
+1 -1
deno.json
··· 2 2 "imports": { 3 3 "$lexicon/": "./__generated__/", 4 4 "@atproto/syntax": "npm:@atproto/syntax@^0.4.0", 5 - "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.11", 5 + "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.12", 6 6 "@gfx/canvas": "jsr:@gfx/canvas@^0.5.8", 7 7 "@std/path": "jsr:@std/path@^1.0.9", 8 8 "@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4",
+17 -17
deno.lock
··· 2 2 "version": "4", 3 3 "specifiers": { 4 4 "jsr:@bigmoves/atproto-oauth-client@0.1": "0.1.0", 5 - "jsr:@bigmoves/bff@0.3.0-beta.11": "0.3.0-beta.11", 5 + "jsr:@bigmoves/bff@0.3.0-beta.12": "0.3.0-beta.12", 6 6 "jsr:@denosaurs/plug@1": "1.0.5", 7 7 "jsr:@denosaurs/plug@1.0.5": "1.0.5", 8 8 "jsr:@gfx/canvas@~0.5.8": "0.5.8", ··· 15 15 "jsr:@std/encoding@0.217.0": "0.217.0", 16 16 "jsr:@std/encoding@^1.0.10": "1.0.10", 17 17 "jsr:@std/fmt@0.214": "0.214.0", 18 - "jsr:@std/fmt@^1.0.7": "1.0.7", 18 + "jsr:@std/fmt@^1.0.8": "1.0.8", 19 19 "jsr:@std/fs@0.214": "0.214.0", 20 20 "jsr:@std/fs@0.217.0": "0.217.0", 21 - "jsr:@std/html@^1.0.3": "1.0.3", 22 - "jsr:@std/http@^1.0.13": "1.0.15", 23 - "jsr:@std/internal@^1.0.6": "1.0.6", 21 + "jsr:@std/html@^1.0.4": "1.0.4", 22 + "jsr:@std/http@^1.0.13": "1.0.16", 23 + "jsr:@std/internal@^1.0.6": "1.0.7", 24 24 "jsr:@std/media-types@^1.1.0": "1.1.0", 25 25 "jsr:@std/net@^1.0.4": "1.0.4", 26 26 "jsr:@std/path@0.214": "0.214.0", ··· 70 70 "npm:jose" 71 71 ] 72 72 }, 73 - "@bigmoves/bff@0.3.0-beta.11": { 74 - "integrity": "1bcdf36eaa440d2cafbf834b37852b4b3f49c97d9802b2307d077cb2f507db5f", 73 + "@bigmoves/bff@0.3.0-beta.12": { 74 + "integrity": "26d404d3db39d2fa187e36a97ebd244ff648152d0820e17b1a2b993da4fbf34b", 75 75 "dependencies": [ 76 76 "jsr:@bigmoves/atproto-oauth-client", 77 77 "jsr:@std/assert@^1.0.13", ··· 140 140 "@std/fmt@0.214.0": { 141 141 "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" 142 142 }, 143 - "@std/fmt@1.0.7": { 144 - "integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb" 143 + "@std/fmt@1.0.8": { 144 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 145 145 }, 146 146 "@std/fs@0.214.0": { 147 147 "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", ··· 157 157 "jsr:@std/path@0.217" 158 158 ] 159 159 }, 160 - "@std/html@1.0.3": { 161 - "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" 160 + "@std/html@1.0.4": { 161 + "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" 162 162 }, 163 - "@std/http@1.0.15": { 164 - "integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159", 163 + "@std/http@1.0.16": { 164 + "integrity": "80c8d08c4bfcf615b89978dcefb84f7e880087cf3b6b901703936f3592a06933", 165 165 "dependencies": [ 166 166 "jsr:@std/cli", 167 167 "jsr:@std/encoding@^1.0.10", 168 - "jsr:@std/fmt@^1.0.7", 168 + "jsr:@std/fmt@^1.0.8", 169 169 "jsr:@std/html", 170 170 "jsr:@std/media-types", 171 171 "jsr:@std/net", ··· 173 173 "jsr:@std/streams" 174 174 ] 175 175 }, 176 - "@std/internal@1.0.6": { 177 - "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" 176 + "@std/internal@1.0.7": { 177 + "integrity": "39eeb5265190a7bc5d5591c9ff019490bd1f2c3907c044a11b0d545796158a0f" 178 178 }, 179 179 "@std/media-types@1.1.0": { 180 180 "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" ··· 1608 1608 }, 1609 1609 "workspace": { 1610 1610 "dependencies": [ 1611 - "jsr:@bigmoves/bff@0.3.0-beta.11", 1611 + "jsr:@bigmoves/bff@0.3.0-beta.12", 1612 1612 "jsr:@gfx/canvas@~0.5.8", 1613 1613 "jsr:@std/path@^1.0.9", 1614 1614 "npm:@atproto/syntax@0.4",
+140 -97
main.tsx
··· 46 46 } from "@bigmoves/bff/components"; 47 47 import { createCanvas, Image } from "@gfx/canvas"; 48 48 import { join } from "@std/path"; 49 - import { formatDistanceStrict } from "date-fns"; 49 + import { 50 + differenceInDays, 51 + differenceInHours, 52 + differenceInMinutes, 53 + differenceInWeeks, 54 + } from "date-fns"; 50 55 import { wrap } from "popmotion"; 51 56 import { ComponentChildren, JSX, VNode } from "preact"; 52 57 ··· 308 313 />, 309 314 ); 310 315 }), 311 - route("/dialogs/image-alt", (req, _params, ctx) => { 312 - const url = new URL(req.url); 313 - const galleryUri = url.searchParams.get("galleryUri"); 314 - const imageCid = url.searchParams.get("imageCid"); 315 - if (!galleryUri || !imageCid) return ctx.next(); 316 - const atUri = new AtUri(galleryUri); 317 - const galleryDid = atUri.hostname; 318 - const galleryRkey = atUri.rkey; 319 - const gallery = getGallery(galleryDid, galleryRkey, ctx); 320 - const photo = gallery?.items?.filter(isPhotoView).find((photo) => { 321 - return photo.cid === imageCid; 322 - }); 323 - if (!photo || !gallery) return ctx.next(); 316 + route("/dialogs/photo/:rkey/alt", (_req, params, ctx) => { 317 + requireAuth(ctx); 318 + const photoRkey = params.rkey; 319 + const photoUri = 320 + `at://${ctx.currentUser.did}/social.grain.photo/${photoRkey}`; 321 + const photo = ctx.indexService.getRecord<WithBffMeta<Photo>>(photoUri); 322 + if (!photo) return ctx.next(); 324 323 return ctx.html( 325 - <PhotoAltDialog galleryUri={gallery.uri} photo={photo} />, 324 + <PhotoAltDialog photo={photoToView(ctx.currentUser.did, photo)} />, 326 325 ); 327 326 }), 328 327 route("/dialogs/photo-select/:galleryRkey", (_req, params, ctx) => { ··· 430 429 key={photo.cid} 431 430 photo={photoToView(photo.did, photo)} 432 431 gallery={gallery} 433 - isCreator={ctx.currentUser.did === gallery.creator.did} 434 - isLoggedIn={!!ctx.currentUser.did} 435 432 /> 436 433 </div> 437 434 <PhotoSelectButton ··· 508 505 }); 509 506 return new Response(null, { status: 200 }); 510 507 }), 508 + route("/actions/photo/:rkey", ["DELETE"], (_req, params, ctx) => { 509 + requireAuth(ctx); 510 + ctx.deleteRecord( 511 + `at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`, 512 + ); 513 + return new Response(null, { status: 200 }); 514 + }), 511 515 route("/actions/favorite", ["POST"], async (req, _params, ctx) => { 512 516 requireAuth(ctx); 513 517 const url = new URL(req.url); ··· 565 569 }); 566 570 567 571 return ctx.redirect(`/profile/${ctx.currentUser.handle}`); 568 - }), 569 - route("/actions/photo/:rkey", ["DELETE"], (_req, params, ctx) => { 570 - requireAuth(ctx); 571 - ctx.deleteRecord( 572 - `at://${ctx.currentUser.did}/social.grain.photo/${params.rkey}`, 573 - ); 574 - return new Response(null, { status: 200 }); 575 572 }), 576 573 ...photoUploadRoutes(), 577 574 ...avatarUploadRoutes(), ··· 1065 1062 {scripts?.map((file) => <script key={file} src={`/static/${file}`} />)} 1066 1063 </head> 1067 1064 <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 1068 - <Layout id="layout" class="dark:border-zinc-800"> 1065 + <Layout id="layout" class="border-zinc-200 dark:border-zinc-800"> 1069 1066 <Layout.Nav 1070 1067 heading={ 1071 1068 <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> 1072 1069 grain 1073 - <sub class="bottom-[0.75rem] text-[1rem] text-sky-500"> 1070 + <sub class="bottom-[0.75rem] text-[1rem]"> 1074 1071 beta 1075 1072 </sub> 1076 1073 </h1> 1077 1074 } 1078 1075 profile={profile} 1079 - class="dark:border-zinc-800" 1076 + class="border-zinc-200 dark:border-zinc-800" 1080 1077 /> 1081 1078 <Layout.Content>{props.children}</Layout.Content> 1082 1079 </Layout> ··· 1139 1136 ); 1140 1137 } 1141 1138 1139 + function ActorInfo({ profile }: Readonly<{ profile: Un$Typed<ProfileView> }>) { 1140 + return ( 1141 + <div class="flex items-center gap-2 min-w-0 flex-1"> 1142 + <img 1143 + src={profile.avatar} 1144 + alt={profile.handle} 1145 + class="rounded-full object-cover size-7 shrink-0" 1146 + /> 1147 + <a 1148 + href={profileLink(profile.handle)} 1149 + class="hover:underline text-zinc-600 dark:text-zinc-500 truncate max-w-[300px] sm:max-w-[400px]" 1150 + > 1151 + <span class="text-zinc-950 dark:text-zinc-50 font-semibold text-"> 1152 + {profile.displayName || profile.handle} 1153 + </span>{" "} 1154 + <span class="truncate"> 1155 + @{profile.handle} 1156 + </span> 1157 + </a> 1158 + </div> 1159 + ); 1160 + } 1161 + 1142 1162 function Timeline({ items }: Readonly<{ items: TimelineItem[] }>) { 1143 1163 return ( 1144 1164 <div class="px-4 mb-4"> ··· 1154 1174 1155 1175 function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) { 1156 1176 return ( 1157 - <li class="space-y-2"> 1158 - <div class="w-fit flex flex-col bg-zinc-100 dark:bg-zinc-900 p-2 gap-2"> 1177 + <li> 1178 + <div class="w-fit flex flex-col gap-4 pb-4 border-b border-zinc-200 dark:border-zinc-800"> 1179 + <div class="flex items-center justify-between gap-2 w-full"> 1180 + <ActorInfo profile={item.actor} /> 1181 + <span class="shrink-0"> 1182 + {formatRelativeTime(new Date(item.createdAt))} 1183 + </span> 1184 + </div> 1159 1185 {item.gallery.items?.filter(isPhotoView).length 1160 1186 ? ( 1161 1187 <a ··· 1206 1232 ) 1207 1233 : null} 1208 1234 <p> 1209 - <a 1210 - href={profileLink(item.actor.handle)} 1211 - class="font-semibold hover:underline" 1212 - > 1213 - @{item.actor.handle} 1214 - </a>{" "} 1215 - {item.itemType === "favorite" ? "favorited" : "created"}{" "} 1235 + {item.itemType === "favorite" ? "Favorited" : "Created"}{" "} 1216 1236 <a 1217 1237 href={galleryLink( 1218 1238 item.gallery.creator.handle, ··· 1222 1242 > 1223 1243 {(item.gallery.record as Gallery).title} 1224 1244 </a> 1225 - <span class="ml-1"> 1226 - {formatDistanceStrict(item.createdAt, new Date(), { 1227 - addSuffix: true, 1228 - })} 1229 - </span> 1230 1245 </p> 1231 1246 </div> 1232 1247 </li> ··· 1266 1281 ); 1267 1282 } 1268 1283 1284 + function formatRelativeTime(date: Date) { 1285 + const now = new Date(); 1286 + const weeks = differenceInWeeks(now, date); 1287 + if (weeks > 0) return `${weeks}w`; 1288 + 1289 + const days = differenceInDays(now, date); 1290 + if (days > 0) return `${days}d`; 1291 + 1292 + const hours = differenceInHours(now, date); 1293 + if (hours > 0) return `${hours}h`; 1294 + 1295 + const minutes = differenceInMinutes(now, date); 1296 + return `${Math.max(1, minutes)}m`; 1297 + } 1298 + 1269 1299 function ProfilePage({ 1270 1300 followUri, 1271 1301 loggedInUserDid, ··· 1282 1312 galleries?: GalleryView[]; 1283 1313 }>) { 1284 1314 const isCreator = loggedInUserDid === profile.did; 1315 + const displayName = profile.displayName || profile.handle; 1285 1316 return ( 1286 1317 <div class="px-4 mb-4" id="profile-page"> 1287 1318 <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4"> 1288 - <div class="flex flex-col"> 1319 + <div class="flex flex-col mb-4"> 1289 1320 <AvatarButton profile={profile} /> 1290 - <p class="text-2xl font-bold">{profile.displayName}</p> 1321 + <p class="text-2xl font-bold">{displayName}</p> 1291 1322 <p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p> 1292 - <p class="my-2">{profile.description}</p> 1323 + {profile.description 1324 + ? <p class="mt-2">{profile.description}</p> 1325 + : null} 1293 1326 </div> 1294 1327 {!isCreator && loggedInUserDid 1295 1328 ? ( ··· 1379 1412 : null} 1380 1413 {selectedTab === "galleries" 1381 1414 ? ( 1382 - <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4"> 1415 + <div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4"> 1383 1416 {galleries?.length 1384 1417 ? ( 1385 1418 galleries.map((gallery) => ( ··· 1443 1476 </a> 1444 1477 )} 1445 1478 </div> 1446 - <div>10/100 photos</div> 1447 1479 </div> 1448 - <Button variant="primary" class="mb-4" asChild> 1449 - <label class="w-fit"> 1480 + <Button variant="primary" class="mb-4 w-full sm:w-fit" asChild> 1481 + <label> 1450 1482 <i class="fa fa-plus"></i> Add photos 1451 1483 <input 1452 1484 class="hidden" ··· 1594 1626 }>) { 1595 1627 const isCreator = currentUserDid === gallery.creator.did; 1596 1628 const isLoggedIn = !!currentUserDid; 1629 + const description = (gallery.record as Gallery).description; 1597 1630 return ( 1598 1631 <div class="px-4"> 1599 - <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4"> 1600 - <div class="flex flex-col space-y-1 mb-4"> 1632 + <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4 mb-2"> 1633 + <div class="flex flex-col space-y-2 mb-4"> 1601 1634 <h1 class="font-bold text-2xl"> 1602 1635 {(gallery.record as Gallery).title} 1603 1636 </h1> 1604 - <div> 1605 - Gallery by{" "} 1606 - <a 1607 - href={profileLink(gallery.creator.handle)} 1608 - class="hover:underline" 1609 - > 1610 - <span class="font-semibold">{gallery.creator.displayName}</span> 1611 - {" "} 1612 - <span class="text-zinc-600 dark:text-zinc-500"> 1613 - @{gallery.creator.handle} 1614 - </span> 1615 - </a> 1616 - </div> 1617 - <p>{(gallery.record as Gallery).description}</p> 1637 + <ActorInfo profile={gallery.creator} /> 1638 + {description ? <p>{description}</p> : null} 1618 1639 </div> 1619 1640 {isLoggedIn && isCreator 1620 1641 ? ( 1621 1642 <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 1622 1643 <Button 1623 - hx-get={`/dialogs/photo-select/${new AtUri(gallery.uri).rkey}`} 1624 - hx-target="#layout" 1625 - hx-swap="afterbegin" 1626 1644 variant="primary" 1627 1645 class="self-start w-full sm:w-fit" 1646 + hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`} 1647 + hx-target="#layout" 1648 + hx-swap="afterbegin" 1628 1649 > 1629 - Add photos 1650 + Edit 1630 1651 </Button> 1631 1652 <Button 1653 + hx-get={`/dialogs/photo-select/${new AtUri(gallery.uri).rkey}`} 1654 + hx-target="#layout" 1655 + hx-swap="afterbegin" 1632 1656 variant="primary" 1633 1657 class="self-start w-full sm:w-fit" 1634 - hx-get={`/dialogs/gallery/${new AtUri(gallery.uri).rkey}`} 1635 - hx-target="#layout" 1636 - hx-swap="afterbegin" 1637 1658 > 1638 - Edit 1659 + Add photos 1639 1660 </Button> 1661 + <ShareGalleryButton gallery={gallery} /> 1640 1662 </div> 1641 1663 ) 1642 1664 : null} 1643 1665 {!isCreator 1644 1666 ? ( 1645 - <FavoriteButton 1646 - currentUserDid={currentUserDid} 1647 - favs={favs} 1648 - galleryUri={gallery.uri} 1649 - /> 1667 + <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 1668 + <ShareGalleryButton gallery={gallery} /> 1669 + <FavoriteButton 1670 + currentUserDid={currentUserDid} 1671 + favs={favs} 1672 + galleryUri={gallery.uri} 1673 + /> 1674 + </div> 1650 1675 ) 1651 1676 : null} 1652 1677 </div> ··· 1663 1688 key={photo.cid} 1664 1689 photo={photo} 1665 1690 gallery={gallery} 1666 - isCreator={isCreator} 1667 - isLoggedIn={isLoggedIn} 1668 1691 /> 1669 1692 )) 1670 1693 : null} ··· 1676 1699 function PhotoButton({ 1677 1700 photo, 1678 1701 gallery, 1679 - isCreator, 1680 - isLoggedIn, 1681 1702 }: Readonly<{ 1682 1703 photo: PhotoView; 1683 1704 gallery: GalleryView; 1684 - isCreator: boolean; 1685 - isLoggedIn: boolean; 1686 1705 }>) { 1687 1706 return ( 1688 1707 <button ··· 1696 1715 data-width={photo.aspectRatio?.width} 1697 1716 data-height={photo.aspectRatio?.height} 1698 1717 > 1699 - {isLoggedIn && isCreator 1700 - ? <AltTextButton galleryUri={gallery.uri} cid={photo.cid} /> 1701 - : null} 1702 1718 <img 1703 1719 src={photo.fullsize} 1704 1720 alt={photo.alt} 1705 1721 class="w-full h-full object-cover" 1706 1722 /> 1707 - {!isCreator && photo.alt 1723 + {photo.alt 1708 1724 ? ( 1709 1725 <div class="absolute bg-zinc-950 dark:bg-zinc-900 bottom-1 right-1 sm:bottom-1 sm:right-1 text-xs text-white font-semibold py-[1px] px-[3px]"> 1710 1726 ALT ··· 1715 1731 ); 1716 1732 } 1717 1733 1734 + function ShareGalleryButton({ 1735 + gallery, 1736 + }: Readonly<{ gallery: GalleryView }>) { 1737 + return ( 1738 + <> 1739 + <input 1740 + type="hidden" 1741 + id="copy-text" 1742 + value={publicGalleryLink(gallery.creator.handle, gallery.uri)} 1743 + /> 1744 + <Button 1745 + variant="primary" 1746 + _={`on click 1747 + set copyText to #copy-text.value 1748 + writeText(copyText) on navigator.clipboard 1749 + alert('Copied to clipboard')`} 1750 + > 1751 + <i class="fa-solid fa-share-nodes mr-2" /> 1752 + Share 1753 + </Button> 1754 + </> 1755 + ); 1756 + } 1757 + 1718 1758 function FavoriteButton({ 1719 1759 currentUserDid, 1720 1760 favs = [], ··· 1844 1884 }>) { 1845 1885 return ( 1846 1886 <div class="relative aspect-square bg-zinc-200 dark:bg-zinc-900"> 1887 + {uri ? <AltTextButton photoUri={uri} /> : null} 1847 1888 {uri 1848 1889 ? ( 1849 1890 <button ··· 1867 1908 } 1868 1909 1869 1910 function AltTextButton({ 1870 - galleryUri, 1871 - cid, 1872 - }: Readonly<{ galleryUri: string; cid: string }>) { 1911 + photoUri, 1912 + }: Readonly<{ photoUri: string }>) { 1873 1913 return ( 1874 1914 <div 1875 - class="bg-zinc-950 dark:bg-zinc-900 py-[1px] px-[3px] absolute top-1 left-1 sm:top-1 sm:left-1 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 1876 - hx-get={`/dialogs/image-alt?galleryUri=${galleryUri}&imageCid=${cid}`} 1915 + class="bg-zinc-950 dark:bg-zinc-950 py-[1px] px-[3px] absolute top-1 left-1 sm:top-1 sm:left-1 cursor-pointer flex items-center justify-center text-xs text-white font-semibold z-10" 1916 + hx-get={`/dialogs/photo/${new AtUri(photoUri).rkey}/alt`} 1877 1917 hx-trigger="click" 1878 1918 hx-target="#layout" 1879 1919 hx-swap="afterbegin" ··· 1942 1982 1943 1983 function PhotoAltDialog({ 1944 1984 photo, 1945 - galleryUri, 1946 1985 }: Readonly<{ 1947 1986 photo: PhotoView; 1948 - galleryUri: string; 1949 1987 }>) { 1950 1988 return ( 1951 1989 <Dialog id="photo-alt-dialog" class="z-30"> ··· 1962 2000 hx-put={`/actions/photo/${new AtUri(photo.uri).rkey}`} 1963 2001 _="on htmx:afterOnLoad trigger closeDialog" 1964 2002 > 1965 - <input type="hidden" name="galleryUri" value={galleryUri} /> 1966 - <input type="hidden" name="cid" value={photo.cid} /> 1967 2003 <div class="my-2"> 1968 2004 <label htmlFor="alt">Descriptive alt text</label> 1969 2005 <Textarea ··· 2072 2108 set @data-added to 'true' 2073 2109 end`} 2074 2110 > 2075 - <div class="hidden group-data-[added=true]:block absolute top-2 right-2"> 2111 + <div class="hidden group-data-[added=true]:block absolute top-2 right-2 z-30"> 2076 2112 <i class="fa-check fa-solid text-sky-500 z-10" /> 2077 2113 </div> 2078 2114 <img ··· 2397 2433 ), 2398 2434 ]; 2399 2435 } 2436 + 2437 + function publicGalleryLink( 2438 + handle: string, 2439 + galleryUri: string, 2440 + ): string { 2441 + return `${PUBLIC_URL}/profile/${handle}/${new AtUri(galleryUri).rkey}`; 2442 + }
+1 -1
static/masonry.js
··· 6 6 const container = document.getElementById("masonry-container"); 7 7 if (!container) return; 8 8 9 - const spacing = 12; 9 + const spacing = 8; 10 10 const containerWidth = container.offsetWidth; 11 11 12 12 if (containerWidth === 0) {
+45 -10
static/styles.css
··· 8 8 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", 9 9 "Courier New", monospace; 10 10 --color-sky-500: oklch(68.5% 0.169 237.323); 11 + --color-zinc-50: oklch(98.5% 0 0); 11 12 --color-zinc-100: oklch(96.7% 0.001 286.375); 12 13 --color-zinc-200: oklch(92% 0.004 286.32); 13 14 --color-zinc-500: oklch(55.2% 0.016 285.938); ··· 281 282 .mt-2 { 282 283 margin-top: calc(var(--spacing) * 2); 283 284 } 285 + .mt-4 { 286 + margin-top: calc(var(--spacing) * 4); 287 + } 284 288 .mr-1 { 285 289 margin-right: calc(var(--spacing) * 1); 286 290 } ··· 292 296 } 293 297 .mb-4 { 294 298 margin-bottom: calc(var(--spacing) * 4); 295 - } 296 - .ml-1 { 297 - margin-left: calc(var(--spacing) * 1); 298 299 } 299 300 .flex { 300 301 display: flex; ··· 314 315 .size-4 { 315 316 width: calc(var(--spacing) * 4); 316 317 height: calc(var(--spacing) * 4); 318 + } 319 + .size-7 { 320 + width: calc(var(--spacing) * 7); 321 + height: calc(var(--spacing) * 7); 317 322 } 318 323 .size-16 { 319 324 width: calc(var(--spacing) * 16); ··· 367 372 .max-w-5xl { 368 373 max-width: var(--container-5xl); 369 374 } 375 + .max-w-\[300px\] { 376 + max-width: 300px; 377 + } 370 378 .max-w-md { 371 379 max-width: var(--container-md); 372 380 } 373 381 .max-w-xl { 374 382 max-width: var(--container-xl); 375 383 } 384 + .min-w-0 { 385 + min-width: calc(var(--spacing) * 0); 386 + } 376 387 .flex-1 { 377 388 flex: 1; 389 + } 390 + .shrink-0 { 391 + flex-shrink: 0; 378 392 } 379 393 .cursor-pointer { 380 394 cursor: pointer; ··· 394 408 .items-center { 395 409 align-items: center; 396 410 } 411 + .justify-between { 412 + justify-content: space-between; 413 + } 397 414 .justify-center { 398 415 justify-content: center; 399 416 } ··· 403 420 .gap-4 { 404 421 gap: calc(var(--spacing) * 4); 405 422 } 406 - .space-y-1 { 407 - :where(& > :not(:last-child)) { 408 - --tw-space-y-reverse: 0; 409 - margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); 410 - margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); 411 - } 412 - } 413 423 .space-y-2 { 414 424 :where(& > :not(:last-child)) { 415 425 --tw-space-y-reverse: 0; ··· 434 444 .self-start { 435 445 align-self: flex-start; 436 446 } 447 + .truncate { 448 + overflow: hidden; 449 + text-overflow: ellipsis; 450 + white-space: nowrap; 451 + } 437 452 .overflow-hidden { 438 453 overflow: hidden; 439 454 } ··· 443 458 .border { 444 459 border-style: var(--tw-border-style); 445 460 border-width: 1px; 461 + } 462 + .border-b { 463 + border-bottom-style: var(--tw-border-style); 464 + border-bottom-width: 1px; 446 465 } 447 466 .border-zinc-200 { 448 467 border-color: var(--color-zinc-200); ··· 500 519 } 501 520 .pt-4 { 502 521 padding-top: calc(var(--spacing) * 4); 522 + } 523 + .pb-4 { 524 + padding-bottom: calc(var(--spacing) * 4); 503 525 } 504 526 .text-center { 505 527 text-align: center; ··· 556 578 .text-zinc-900 { 557 579 color: var(--color-zinc-900); 558 580 } 581 + .text-zinc-950 { 582 + color: var(--color-zinc-950); 583 + } 559 584 .lowercase { 560 585 text-transform: lowercase; 561 586 } ··· 625 650 width: fit-content; 626 651 } 627 652 } 653 + .sm\:max-w-\[400px\] { 654 + @media (width >= 40rem) { 655 + max-width: 400px; 656 + } 657 + } 628 658 .sm\:grid-cols-3 { 629 659 @media (width >= 40rem) { 630 660 grid-template-columns: repeat(3, minmax(0, 1fr)); ··· 678 708 .dark\:text-white { 679 709 @media (prefers-color-scheme: dark) { 680 710 color: var(--color-white); 711 + } 712 + } 713 + .dark\:text-zinc-50 { 714 + @media (prefers-color-scheme: dark) { 715 + color: var(--color-zinc-50); 681 716 } 682 717 } 683 718 .dark\:text-zinc-500 {