Something went wrong. Try again.
grain.social is a photo sharing platform built on atproto. grain.social
appview atproto photography
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697<script lang="ts"> import type { Snippet } from 'svelte' import Sidebar from '../organisms/Sidebar.svelte' import PublicTopBar from '../molecules/PublicTopBar.svelte' import SearchFlyout from '../organisms/SearchFlyout.svelte' import MobileTopBar from '../molecules/MobileTopBar.svelte' import MobileBottomBar from '../molecules/MobileBottomBar.svelte' import IosAppBanner from '../molecules/IosAppBanner.svelte' import MobileDrawer from '../organisms/MobileDrawer.svelte' import MobileSearch from '../organisms/MobileSearch.svelte' import LoginModal from '../organisms/LoginModal.svelte' import { loginModalOpen } from '$lib/stores' import { page } from '$app/state' import { afterNavigate } from '$app/navigation'
let { children }: { children: Snippet } = $props()
let drawerOpen = $state(false) let searchOpen = $state(false) let searchFlyoutOpen = $state(false)
// The nav stays clickable beside the open panel, so a nav click has to // dismiss it — otherwise the panel outlives the page it was opened from. afterNavigate(() => (searchFlyoutOpen = false))
// Routes opt into shell variations by returning flags from their load, so // route knowledge stays with the route rather than in the shell. // wide: true -> 935px content column instead of 600px // full: true -> no left nav; a public top bar carries the wordmark and // sign-in instead. Routes set this only when signed out. // rail: true -> nav collapses to icons (routes with their own sub-nav) const contentMax = $derived(page.data?.wide ? '935px' : null) const full = $derived(page.data?.full === true) const rail = $derived(page.data?.rail === true)</script>
<IosAppBanner /><MobileTopBar onHamburger={() => drawerOpen = true} onSearch={() => searchOpen = true} />
{#if full} <PublicTopBar maxWidth={contentMax ?? '600px'} />{/if}
<div class="shell" class:full style:--col-left={rail ? '78px' : null}> {#if !full} <Sidebar {rail} onSearch={() => (searchFlyoutOpen = !searchFlyoutOpen)} searchOpen={searchFlyoutOpen} /> {/if} <main class="col-center" style:--content-max={contentMax}> {@render children()} </main></div>
<MobileBottomBar /><MobileDrawer bind:open={drawerOpen} /><MobileSearch bind:open={searchOpen} /><SearchFlyout bind:open={searchFlyoutOpen} navWidth={rail ? '78px' : '245px'} /><LoginModal bind:open={$loginModalOpen} />
<style> /* The nav owns a fixed track and the content column takes the rest, so the nav sits in the same place on every route. Width is chosen by the content itself via --content-max, not by the grid — otherwise a wider page would shift the nav sideways as you navigated. */ .shell { display: grid; grid-template-columns: var(--col-left) minmax(0, 1fr); gap: 0; max-width: var(--shell-max); margin: 0 auto; min-height: 100vh; } .shell.full { grid-template-columns: minmax(0, 1fr); } .col-center { width: 100%; max-width: var(--content-max, 600px); margin-inline: auto; min-height: 100vh; min-width: 0; overflow-wrap: anywhere; } @media (max-width: 600px) { .shell { grid-template-columns: 1fr; } .col-center { position: fixed; top: calc(47px + var(--ios-banner-h, 0px)); left: 0; right: 0; bottom: calc(50px + env(safe-area-inset-bottom, 0px)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; min-height: 0; border-right: none; } }</style>