Your calm window into the Atmosphere. morgen.blue
rss atproto
3
fork

Configure Feed

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

feat(layout): add window-layout shell + /consume, /discover, /create

Introduces the Morgenblau Window-framed authenticated shell distinct
from the starter-pack sidebar. Top chrome carries the three section
tabs and right-aligned add-source / profile / settings icon buttons;
the Window is the only scroll surface so the page itself never moves.

Wires the three new Inertia pages through app.tsx's layout switch and
adds matching auth-protected routes. Footer stamp uses Caveat secondary
at the new text-2xs token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+136
+3
resources/css/app.css
··· 45 45 --color-sand-brown: oklch(0.9579 0.0301 69.05); 46 46 47 47 --color-brand: var(--color-atmosphere-blue); 48 + 49 + --text-2xs: 0.625rem; 50 + --text-2xs--line-height: calc(0.875 / 0.625); 48 51 } 49 52 50 53 @theme inline {
+5
resources/js/app.tsx
··· 6 6 import AuthGoldenLayout from '@/layouts/auth/auth-golden-layout'; 7 7 import AuthLayout from '@/layouts/auth-layout'; 8 8 import SettingsLayout from '@/layouts/settings/layout'; 9 + import WindowLayout from '@/layouts/window-layout'; 10 + 11 + const WINDOW_PAGES = new Set(['discover', 'consume', 'create']); 9 12 10 13 const appName = import.meta.env.VITE_APP_NAME || 'Morgenblau'; 11 14 ··· 21 24 return AuthLayout; 22 25 case name.startsWith('settings/'): 23 26 return [AppLayout, SettingsLayout]; 27 + case WINDOW_PAGES.has(name): 28 + return WindowLayout; 24 29 default: 25 30 return AppLayout; 26 31 }
+69
resources/js/components/window-chrome.tsx
··· 1 + import { 2 + PlusSignIcon, 3 + Settings02Icon, 4 + UserCircleIcon, 5 + } from '@hugeicons/core-free-icons'; 6 + import { HugeiconsIcon } from '@hugeicons/react'; 7 + import { Link, usePage } from '@inertiajs/react'; 8 + 9 + import { Button } from '@/components/ui/button'; 10 + import { cn } from '@/lib/utils'; 11 + import { consume, create, discover } from '@/routes'; 12 + 13 + type Tab = { 14 + label: string; 15 + href: string; 16 + }; 17 + 18 + const TABS: Tab[] = [ 19 + { label: 'Discover', href: discover().url }, 20 + { label: 'Consume', href: consume().url }, 21 + { label: 'Create', href: create().url }, 22 + ]; 23 + 24 + export function WindowChrome() { 25 + const { url } = usePage(); 26 + 27 + return ( 28 + <header className="flex h-14 shrink-0 items-center justify-between px-4"> 29 + <nav className="flex items-center gap-6"> 30 + {TABS.map((tab) => { 31 + const isActive = url === tab.href; 32 + 33 + return ( 34 + <Link 35 + key={tab.href} 36 + href={tab.href} 37 + className={cn( 38 + 'relative text-sm font-medium transition-colors outline-none focus-visible:outline-1 focus-visible:outline-offset-4 focus-visible:outline-ring focus-visible:outline-solid', 39 + isActive 40 + ? 'text-foreground' 41 + : 'text-muted-foreground hover:text-foreground', 42 + )} 43 + > 44 + {tab.label} 45 + {isActive && ( 46 + <span 47 + aria-hidden 48 + className="absolute -bottom-2 left-1/2 size-1 -translate-x-1/2 rounded-full bg-primary" 49 + /> 50 + )} 51 + </Link> 52 + ); 53 + })} 54 + </nav> 55 + 56 + <div className="flex items-center gap-1"> 57 + <Button variant="ghost" size="icon-sm" aria-label="Add source"> 58 + <HugeiconsIcon icon={PlusSignIcon} /> 59 + </Button> 60 + <Button variant="ghost" size="icon-sm" aria-label="Profile"> 61 + <HugeiconsIcon icon={UserCircleIcon} /> 62 + </Button> 63 + <Button variant="ghost" size="icon-sm" aria-label="Settings"> 64 + <HugeiconsIcon icon={Settings02Icon} /> 65 + </Button> 66 + </div> 67 + </header> 68 + ); 69 + }
+14
resources/js/components/window-footer.tsx
··· 1 + import AppLogoIcon from '@/components/app-logo-icon'; 2 + 3 + export function WindowFooter() { 4 + return ( 5 + <footer className="flex flex-col items-center gap-1.5 py-6"> 6 + <div className="grid size-6 place-items-center rounded-full bg-foreground/[0.04] text-muted-foreground/70 shadow-[inset_0_1px_1.5px_rgba(0,0,0,0.06)] dark:bg-white/[0.04] dark:shadow-[inset_0_1px_1.5px_rgba(0,0,0,0.4)]"> 7 + <AppLogoIcon className="size-3.5" /> 8 + </div> 9 + <p className="font-handwritten text-2xs text-muted-foreground"> 10 + Your calm window into the Atmosphere. 11 + </p> 12 + </footer> 13 + ); 14 + }
+27
resources/js/layouts/window-layout.tsx
··· 1 + import type { ReactNode } from 'react'; 2 + 3 + import { Window } from '@/components/window'; 4 + import { WindowChrome } from '@/components/window-chrome'; 5 + import { WindowFooter } from '@/components/window-footer'; 6 + 7 + type WindowLayoutProps = { 8 + children: ReactNode; 9 + }; 10 + 11 + export default function WindowLayout({ children }: WindowLayoutProps) { 12 + return ( 13 + <div className="flex h-dvh flex-col"> 14 + <WindowChrome /> 15 + <main className="min-h-0 flex-1 px-4 pb-4"> 16 + <Window variant="plain" className="h-full"> 17 + <div className="h-full overflow-y-auto"> 18 + <div className="flex min-h-full flex-col"> 19 + <div className="flex-1">{children}</div> 20 + <WindowFooter /> 21 + </div> 22 + </div> 23 + </Window> 24 + </main> 25 + </div> 26 + ); 27 + }
+5
resources/js/pages/consume.tsx
··· 1 + import { Head } from '@inertiajs/react'; 2 + 3 + export default function Consume() { 4 + return <Head title="Consume" />; 5 + }
+5
resources/js/pages/create.tsx
··· 1 + import { Head } from '@inertiajs/react'; 2 + 3 + export default function Create() { 4 + return <Head title="Create" />; 5 + }
+5
resources/js/pages/discover.tsx
··· 1 + import { Head } from '@inertiajs/react'; 2 + 3 + export default function Discover() { 4 + return <Head title="Discover" />; 5 + }
+3
routes/web.php
··· 38 38 39 39 Route::middleware(['auth'])->group(function () { 40 40 Route::inertia('dashboard', 'dashboard')->name('dashboard'); 41 + Route::inertia('discover', 'discover')->name('discover'); 42 + Route::inertia('consume', 'consume')->name('consume'); 43 + Route::inertia('create', 'create')->name('create'); 41 44 }); 42 45 43 46 require __DIR__.'/settings.php';