grain.social is a photo sharing platform built on atproto.

Update layout

Expands navbar fullscreen for a more modern look. Same layout on mobile where the navbar flows with the scroll (not fixed).

![screenshot](https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreihssk6un6npe4apd6lxcwcik6a5gi6p2cp4fhjhaxcsoupt6lsfiu@jpeg)

+3 -4
src/app.tsx
··· 12 undefined; 13 const staticFilesHash = props.ctx.state.staticFilesHash; 14 return ( 15 - <html lang="en" class="w-full h-full"> 16 <head> 17 <meta charset="UTF-8" /> 18 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> ··· 56 /> 57 ))} 58 </head> 59 - <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 60 - <Layout id="layout" class="border-zinc-200 dark:border-zinc-800"> 61 <Layout.Nav 62 heading={ 63 <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> ··· 67 } 68 profile={profile} 69 hasNotifications={hasNotifications} 70 - class="border-zinc-200 dark:border-zinc-800" 71 /> 72 <Layout.Content>{props.children}</Layout.Content> 73 </Layout>
··· 12 undefined; 13 const staticFilesHash = props.ctx.state.staticFilesHash; 14 return ( 15 + <html lang="en" class="h-full"> 16 <head> 17 <meta charset="UTF-8" /> 18 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> ··· 56 /> 57 ))} 58 </head> 59 + <body class="h-full dark:bg-zinc-950 dark:text-white"> 60 + <Layout id="layout"> 61 <Layout.Nav 62 heading={ 63 <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> ··· 67 } 68 profile={profile} 69 hasNotifications={hasNotifications} 70 /> 71 <Layout.Content>{props.children}</Layout.Content> 72 </Layout>
+1 -1
src/components/AvatarDialog.tsx
··· 7 profile, 8 }: Readonly<{ profile: Un$Typed<ProfileView> }>) { 9 return ( 10 - <Dialog> 11 <Dialog.X /> 12 <div 13 class="w-[400px] h-[400px] flex flex-col p-4 z-10"
··· 7 profile, 8 }: Readonly<{ profile: Un$Typed<ProfileView> }>) { 9 return ( 10 + <Dialog class="z-100"> 11 <Dialog.X /> 12 <div 13 class="w-[400px] h-[400px] flex flex-col p-4 z-10"
+1 -1
src/components/GalleryCreateEditDialog.tsx
··· 6 gallery, 7 }: Readonly<{ gallery?: GalleryView | null }>) { 8 return ( 9 - <Dialog id="gallery-dialog" class="z-30"> 10 <Dialog.Content class="dark:bg-zinc-950 relative"> 11 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 12 <Dialog.Title>
··· 6 gallery, 7 }: Readonly<{ gallery?: GalleryView | null }>) { 8 return ( 9 + <Dialog id="gallery-dialog" class="z-100"> 10 <Dialog.Content class="dark:bg-zinc-950 relative"> 11 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 12 <Dialog.Title>
+1 -1
src/components/GallerySortDialog.tsx
··· 7 { gallery }: Readonly<{ gallery: GalleryView }>, 8 ) { 9 return ( 10 - <Dialog> 11 <Dialog.Content class="dark:bg-zinc-950 relative"> 12 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 13 <Dialog.Title>Sort gallery</Dialog.Title>
··· 7 { gallery }: Readonly<{ gallery: GalleryView }>, 8 ) { 9 return ( 10 + <Dialog class="z-100"> 11 <Dialog.Content class="dark:bg-zinc-950 relative"> 12 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 13 <Dialog.Title>Sort gallery</Dialog.Title>
+60 -55
src/components/Layout.tsx
··· 25 return ( 26 <div 27 class={cn( 28 - "h-full max-w-5xl mx-auto sm:border-x relative", 29 classProp, 30 )} 31 {...props} ··· 40 ) => { 41 return ( 42 <main 43 - class={cn("h-[calc(100vh-56px)] sm:overflow-y-auto", classProp)} 44 {...props} 45 > 46 {children} ··· 60 return ( 61 <nav 62 class={cn( 63 - "w-full border-b border-zinc-950 flex justify-between items-center px-4 h-14", 64 classProp, 65 )} 66 {...props} 67 > 68 - <div class="flex items-center space-x-4"> 69 - <a href="/"> 70 - {heading} 71 - </a> 72 - </div> 73 - <div class="space-x-2"> 74 - {profile 75 - ? ( 76 - <div class="flex items-center ts:space-x-1 sm:space-x-2"> 77 - <form hx-post="/logout" hx-swap="none" class="inline"> 78 - <Button type="submit" variant="secondary">Sign out</Button> 79 - </form> 80 - <Button 81 - asChild 82 - variant="secondary" 83 - class="relative pl-2" 84 - > 85 - <a href="/explore"> 86 - <i class="fas fa-search text-zinc-950 dark:text-zinc-50" /> 87 </a> 88 - </Button> 89 - <Button 90 - asChild 91 - variant="secondary" 92 - class="relative pl-2" 93 - > 94 - <a href="/notifications"> 95 - <i class="fas fa-bell text-zinc-950 dark:text-zinc-50" /> 96 - {hasNotifications 97 - ? ( 98 - <span class="absolute inline-flex items-center justify-center w-3 h-3 text-xs font-bold text-white bg-sky-500 rounded-full top-1 right-1" /> 99 - ) 100 - : null} 101 - </a> 102 - </Button> 103 - <a href={`/profile/${profile.handle}`}> 104 - <ActorAvatar profile={profile} size={32} /> 105 - </a> 106 - </div> 107 - ) 108 - : ( 109 - <div class="flex items-center space-x-4"> 110 - <form hx-post="/signup" hx-swap="none" class="inline"> 111 - <Button variant="secondary" type="submit"> 112 - Create account 113 </Button> 114 - </form> 115 - <Button variant="secondary" asChild> 116 - <a href="/login"> 117 - Sign in 118 - </a> 119 - </Button> 120 - </div> 121 - )} 122 </div> 123 </nav> 124 );
··· 25 return ( 26 <div 27 class={cn( 28 + "min-h-screen flex flex-col max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800", 29 classProp, 30 )} 31 {...props} ··· 40 ) => { 41 return ( 42 <main 43 + class={cn( 44 + "flex-1 sm:pt-14", // pt-14 pushes content below the fixed nav 45 + classProp, 46 + )} 47 {...props} 48 > 49 {children} ··· 63 return ( 64 <nav 65 class={cn( 66 + "sm:fixed sm:top-0 sm:left-0 sm:right-0 h-14 z-50 bg-white dark:bg-zinc-950 border-b border-zinc-200 dark:border-zinc-800", 67 classProp, 68 )} 69 {...props} 70 > 71 + <div class="mx-auto max-w-5xl h-full flex items-center justify-between px-4"> 72 + <div class="flex items-center space-x-4"> 73 + <a href="/"> 74 + {heading} 75 + </a> 76 + </div> 77 + <div class="flex space-x-2"> 78 + {profile 79 + ? ( 80 + <div class="flex items-center ts:space-x-1 sm:space-x-2"> 81 + <form hx-post="/logout" hx-swap="none" class="inline"> 82 + <Button type="submit" variant="secondary">Sign out</Button> 83 + </form> 84 + <Button 85 + asChild 86 + variant="secondary" 87 + class="relative pl-2" 88 + > 89 + <a href="/explore"> 90 + <i class="fas fa-search text-zinc-950 dark:text-zinc-50" /> 91 + </a> 92 + </Button> 93 + <Button 94 + asChild 95 + variant="secondary" 96 + class="relative pl-2" 97 + > 98 + <a href="/notifications"> 99 + <i class="fas fa-bell text-zinc-950 dark:text-zinc-50" /> 100 + {hasNotifications 101 + ? ( 102 + <span class="absolute inline-flex items-center justify-center w-3 h-3 text-xs font-bold text-white bg-sky-500 rounded-full top-1 right-1" /> 103 + ) 104 + : null} 105 + </a> 106 + </Button> 107 + <a href={`/profile/${profile.handle}`}> 108 + <ActorAvatar profile={profile} size={32} /> 109 </a> 110 + </div> 111 + ) 112 + : ( 113 + <div class="flex items-center space-x-4"> 114 + <form hx-post="/signup" hx-swap="none" class="inline"> 115 + <Button variant="secondary" type="submit"> 116 + Create account 117 + </Button> 118 + </form> 119 + <Button variant="secondary" asChild> 120 + <a href="/login"> 121 + Sign in 122 + </a> 123 </Button> 124 + </div> 125 + )} 126 + </div> 127 </div> 128 </nav> 129 );
+1 -1
src/components/LoginPage.tsx
··· 5 return ( 6 <div 7 id="login" 8 - class="flex justify-center items-center w-full h-full relative" 9 style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg'); background-size: cover; background-position: center;" 10 > 11 <Login hx-target="#login" error={error} errorClass="text-white" />
··· 5 return ( 6 <div 7 id="login" 8 + class="flex justify-center items-center w-full h-[calc(100vh-56px)] relative" 9 style="background-image: url('https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:bcgltzqazw5tb6k2g3ttenbj/bafkreiewhwu3ro5dv7omedphb62db4koa7qtvyzfhiiypg3ru4tvuxkrjy@jpeg'); background-size: cover; background-position: center;" 10 > 11 <Login hx-target="#login" error={error} errorClass="text-white" />
+1 -1
src/components/PhotoAltDialog.tsx
··· 8 photo: PhotoView; 9 }>) { 10 return ( 11 - <Dialog id="photo-alt-dialog" class="z-30"> 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 <Dialog.Title>Add alt text</Dialog.Title>
··· 8 photo: PhotoView; 9 }>) { 10 return ( 11 + <Dialog id="photo-alt-dialog" class="z-100"> 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 <Dialog.Title>Add alt text</Dialog.Title>
+1 -1
src/components/PhotoDialog.tsx
··· 15 prevImage?: PhotoView; 16 }>) { 17 return ( 18 - <Dialog id="photo-dialog" class="bg-zinc-950 z-30"> 19 <Dialog.X /> 20 {nextImage 21 ? (
··· 15 prevImage?: PhotoView; 16 }>) { 17 return ( 18 + <Dialog id="photo-dialog" class="bg-zinc-950 z-100"> 19 <Dialog.X /> 20 {nextImage 21 ? (
+1 -1
src/components/PhotoSelectDialog.tsx
··· 13 photos: PhotoView[]; 14 }>) { 15 return ( 16 - <Dialog id="photo-select-dialog" class="z-30"> 17 <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col relative"> 18 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 19 <Dialog.Title>Add photos</Dialog.Title>
··· 13 photos: PhotoView[]; 14 }>) { 15 return ( 16 + <Dialog id="photo-select-dialog" class="z-100"> 17 <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950 sm:min-h-screen flex flex-col relative"> 18 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 19 <Dialog.Title>Add photos</Dialog.Title>
+1 -1
src/components/ProfileDialog.tsx
··· 8 profile: ProfileView; 9 }>) { 10 return ( 11 - <Dialog> 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 <Dialog.Title>Edit my profile</Dialog.Title>
··· 8 profile: ProfileView; 9 }>) { 10 return ( 11 + <Dialog class="z-100"> 12 <Dialog.Content class="dark:bg-zinc-950 relative"> 13 <Dialog.X class="fill-zinc-950 dark:fill-zinc-50" /> 14 <Dialog.Title>Edit my profile</Dialog.Title>
+1 -1
src/routes/explore.tsx
··· 53 <div class="my-4"> 54 <Input 55 name="q" 56 - class="dark:bg-zinc-800 dark:text-white" 57 placeholder="Search for users" 58 hx-get="/explore" 59 hx-target="#search-results"
··· 53 <div class="my-4"> 54 <Input 55 name="q" 56 + class="dark:bg-zinc-800 dark:text-white border-zinc-100 bg-zinc-100 dark:border-zinc-800" 57 placeholder="Search for users" 58 hx-get="/explore" 59 hx-target="#search-results"
+43 -8
static/styles.css
··· 198 .absolute { 199 position: absolute; 200 } 201 .relative { 202 position: relative; 203 } ··· 248 } 249 .z-30 { 250 z-index: 30; 251 } 252 .container { 253 width: 100%; ··· 354 } 355 .h-full { 356 height: 100%; 357 } 358 .w-1\/3 { 359 width: calc(1/3 * 100%); ··· 536 .border-zinc-900 { 537 border-color: var(--color-zinc-900); 538 } 539 - .border-zinc-950 { 540 - border-color: var(--color-zinc-950); 541 - } 542 .bg-black { 543 background-color: var(--color-black); 544 } ··· 550 } 551 .bg-sky-500 { 552 background-color: var(--color-sky-500); 553 } 554 .bg-zinc-100 { 555 background-color: var(--color-zinc-100); ··· 595 } 596 .pt-4 { 597 padding-top: calc(var(--spacing) * 4); 598 } 599 .pb-4 { 600 padding-bottom: calc(var(--spacing) * 4); ··· 710 opacity: 50%; 711 } 712 } 713 .sm\:right-1 { 714 @media (width >= 40rem) { 715 right: calc(var(--spacing) * 1); ··· 718 .sm\:bottom-1 { 719 @media (width >= 40rem) { 720 bottom: calc(var(--spacing) * 1); 721 } 722 } 723 .sm\:h-screen { ··· 789 } 790 } 791 } 792 - .sm\:overflow-y-auto { 793 - @media (width >= 40rem) { 794 - overflow-y: auto; 795 - } 796 - } 797 .sm\:border-x { 798 @media (width >= 40rem) { 799 border-inline-style: var(--tw-border-style); ··· 803 .sm\:px-0 { 804 @media (width >= 40rem) { 805 padding-inline: calc(var(--spacing) * 0); 806 } 807 } 808 .dark\:divide-zinc-800 {
··· 198 .absolute { 199 position: absolute; 200 } 201 + .fixed { 202 + position: fixed; 203 + } 204 .relative { 205 position: relative; 206 } ··· 251 } 252 .z-30 { 253 z-index: 30; 254 + } 255 + .z-50 { 256 + z-index: 50; 257 + } 258 + .z-100 { 259 + z-index: 100; 260 } 261 .container { 262 width: 100%; ··· 363 } 364 .h-full { 365 height: 100%; 366 + } 367 + .min-h-screen { 368 + min-height: 100vh; 369 } 370 .w-1\/3 { 371 width: calc(1/3 * 100%); ··· 548 .border-zinc-900 { 549 border-color: var(--color-zinc-900); 550 } 551 .bg-black { 552 background-color: var(--color-black); 553 } ··· 559 } 560 .bg-sky-500 { 561 background-color: var(--color-sky-500); 562 + } 563 + .bg-white { 564 + background-color: var(--color-white); 565 } 566 .bg-zinc-100 { 567 background-color: var(--color-zinc-100); ··· 607 } 608 .pt-4 { 609 padding-top: calc(var(--spacing) * 4); 610 + } 611 + .pt-14 { 612 + padding-top: calc(var(--spacing) * 14); 613 } 614 .pb-4 { 615 padding-bottom: calc(var(--spacing) * 4); ··· 725 opacity: 50%; 726 } 727 } 728 + .sm\:fixed { 729 + @media (width >= 40rem) { 730 + position: fixed; 731 + } 732 + } 733 + .sm\:top-0 { 734 + @media (width >= 40rem) { 735 + top: calc(var(--spacing) * 0); 736 + } 737 + } 738 + .sm\:right-0 { 739 + @media (width >= 40rem) { 740 + right: calc(var(--spacing) * 0); 741 + } 742 + } 743 .sm\:right-1 { 744 @media (width >= 40rem) { 745 right: calc(var(--spacing) * 1); ··· 748 .sm\:bottom-1 { 749 @media (width >= 40rem) { 750 bottom: calc(var(--spacing) * 1); 751 + } 752 + } 753 + .sm\:left-0 { 754 + @media (width >= 40rem) { 755 + left: calc(var(--spacing) * 0); 756 } 757 } 758 .sm\:h-screen { ··· 824 } 825 } 826 } 827 .sm\:border-x { 828 @media (width >= 40rem) { 829 border-inline-style: var(--tw-border-style); ··· 833 .sm\:px-0 { 834 @media (width >= 40rem) { 835 padding-inline: calc(var(--spacing) * 0); 836 + } 837 + } 838 + .sm\:pt-14 { 839 + @media (width >= 40rem) { 840 + padding-top: calc(var(--spacing) * 14); 841 } 842 } 843 .dark\:divide-zinc-800 {