Layout primitives
App Shell code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/app-shell.blade.php
@props([
'title' => null,
'description' => null,
'contained' => false,
'transparentHeader' => false,
'transparentFooter' => false,
'sunken' => false,
'hybrid' => false,
])
@php
/*
| Full authenticated app frame for SaaS/admin screens. Compose sidebar rows
| with x-ui.sidebar.item and keep page content in the default slot.
|
| Usage:
| <x-ui.app-shell title="Dashboard">
| <x-slot:sidebar>...</x-slot:sidebar>
| <x-slot:sidebarFooter>Utility actions such as notifications.</x-slot:sidebarFooter>
| <x-slot:topbar>Search field or command-palette trigger.</x-slot:topbar>
| <x-slot:actions>Page-level header actions.</x-slot:actions>
| <x-slot:user>User avatar or account menu.</x-slot:user>
| <x-slot:footer>...</x-slot:footer>
|
| When a sidebar is provided, theme controls and the user slot render in the
| sidebar footer instead of the top bar, and the top bar is hidden on desktop
| (lg+) so the page header carries title and actions without duplication.
|
| hybrid keeps that sidebar and also shows a desktop utility top bar. Theme
| controls, the user slot, and sidebarFooter then move into the top bar.
| Use the topbar slot for search.
|
| transparentHeader — drop the page header background (keeps a subtle border)
| transparentFooter — drop the footer background (keeps a subtle border)
| sunken — recess the sidebar and lift the page into one large card
| hybrid — keep the utility top bar visible on desktop with a sidebar
|
| </x-ui.app-shell>
*/
$hasSidebar = isset($sidebar);
$chromeInSidebar = $hasSidebar && ! $hybrid;
$chrome = $hybrid ? 'hybrid' : ($hasSidebar ? 'sidebar' : 'topbar');
$rootClasses = implode(' ', [
$contained ? 'h-full min-h-0' : 'min-h-screen',
$sunken
? 'bg-gray-100 text-gray-900 dark:bg-gray-950 dark:text-gray-100'
: 'bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100',
]);
@endphp
<div
x-data="{ sidebarOpen: false }"
data-app-shell-chrome="{{ $chrome }}"
{{ $attributes->merge(['class' => $rootClasses]) }}
>
<div
x-show="sidebarOpen"
x-cloak
class="fixed inset-0 z-40 bg-gray-950/60 backdrop-blur-sm lg:hidden"
x-transition.opacity
x-on:click="sidebarOpen = false"
></div>
<aside
x-show="sidebarOpen"
x-cloak
class="fixed inset-y-0 start-0 z-50 flex w-72 flex-col border-e border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-900 lg:hidden"
x-transition:enter="transform transition ease-out duration-200"
x-transition:enter-start="-translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in duration-150"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="-translate-x-full"
aria-label="{{ __('Mobile navigation') }}"
>
<div class="flex h-16 shrink-0 items-center justify-between border-b border-gray-200 px-5 dark:border-gray-800">
@isset($brand)
{{ $brand }}
@else
<x-ui.brand />
@endisset
<x-ui.icon-button icon="x" label="Close navigation" size="sm" x-on:click="sidebarOpen = false" />
</div>
<nav class="flex-1 min-h-0 space-y-1 overflow-y-auto p-3">
{{ $sidebar ?? '' }}
</nav>
@if ($chromeInSidebar)
<x-ui.app-shell.sidebar-footer>
@isset($user)
<x-slot:user>{{ $user }}</x-slot:user>
@endisset
@isset($sidebarFooter)
<x-slot:sidebarFooter>{{ $sidebarFooter }}</x-slot:sidebarFooter>
@endisset
</x-ui.app-shell.sidebar-footer>
@endif
</aside>
<div @class([
$contained ? 'h-full min-h-0' : 'min-h-screen',
'lg:grid lg:grid-cols-[16rem_1fr]' => ! $sunken,
'lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-2 lg:p-2 xl:grid-cols-[16rem_minmax(0,1fr)]' => $sunken,
])>
<aside @class([
'hidden lg:flex lg:flex-col',
'border-e border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900' => ! $sunken,
'bg-transparent' => $sunken,
'h-full min-h-0' => $contained,
'min-h-full' => ! $contained && ! $sunken,
'h-[calc(100vh-1rem)] min-h-0' => ! $contained && $sunken,
]) aria-label="{{ __('Sidebar navigation') }}">
<div @class([
'flex min-h-0 flex-col',
'h-full' => $contained,
'lg:sticky lg:top-0 lg:h-screen' => ! $contained && ! $sunken,
'h-[calc(100vh-1rem)]' => ! $contained && $sunken,
])>
<div @class([
'flex h-16 shrink-0 items-center px-5',
'border-b border-gray-200 dark:border-gray-800' => ! $sunken,
])>
@isset($brand)
{{ $brand }}
@else
<x-ui.brand />
@endisset
</div>
<nav class="flex-1 min-h-0 space-y-1 overflow-y-auto p-3">
{{ $sidebar ?? '' }}
</nav>
@if ($chromeInSidebar)
<x-ui.app-shell.sidebar-footer :integrated="$sunken">
@isset($user)
<x-slot:user>{{ $user }}</x-slot:user>
@endisset
@isset($sidebarFooter)
<x-slot:sidebarFooter>{{ $sidebarFooter }}</x-slot:sidebarFooter>
@endisset
</x-ui.app-shell.sidebar-footer>
@endif
</div>
</aside>
<div @class([
'min-w-0 flex flex-col',
'bg-white dark:bg-gray-900 lg:overflow-hidden lg:rounded-2xl lg:border lg:border-gray-200/90 lg:shadow-[0_1px_2px_rgba(15,23,42,0.04),0_12px_36px_rgba(15,23,42,0.06)] dark:lg:border-gray-800 dark:lg:shadow-[0_1px_2px_rgba(0,0,0,0.4),0_16px_44px_rgba(0,0,0,0.28)]' => $sunken,
'h-full min-h-0 overflow-auto' => $contained,
'min-h-screen' => ! $contained && ! $sunken,
'min-h-screen lg:h-[calc(100vh-1rem)] lg:min-h-0 lg:overflow-y-auto' => ! $contained && $sunken,
])>
<header @class([
'sticky top-0 z-30 border-b border-gray-200 bg-white/90 backdrop-blur dark:border-gray-800 dark:bg-gray-900/90',
'lg:hidden' => $chromeInSidebar,
]) @if ($hybrid) aria-label="{{ __('Utility toolbar') }}" @endif>
<div class="flex h-16 items-center gap-4 px-4 sm:px-6 lg:px-8">
<div @class([
'flex items-center',
'lg:hidden' => $hybrid && isset($header),
'min-w-0' => ! $hybrid || ! isset($header),
'gap-3' => ! $sunken,
'gap-4 sm:gap-6' => $sunken,
])>
<x-ui.icon-button
icon="list"
label="Open navigation"
:variant="$sunken ? 'outline' : 'ghost'"
:size="$sunken ? 'base' : 'sm'"
class="lg:hidden"
data-sidebar-trigger-style="{{ $sunken ? 'emphasized' : 'default' }}"
x-on:click="sidebarOpen = true"
/>
@if (! $hybrid || ! isset($header))
<div class="min-w-0">
@if ($title)
<h1 class="truncate text-base font-semibold text-gray-900 dark:text-white">{{ $title }}</h1>
@endif
@if ($description)
<p class="truncate text-xs text-gray-500 dark:text-gray-400">{{ $description }}</p>
@endif
</div>
@endif
</div>
@isset($topbar)
<div class="min-w-0 flex-1">{{ $topbar }}</div>
@endisset
<div @class([
'flex shrink-0 items-center gap-2',
'ms-auto' => ! isset($topbar),
])>
@isset($actions)
{{ $actions }}
@endisset
@if ($hybrid)
@isset($sidebarFooter)
{{ $sidebarFooter }}
@endisset
<x-theme-switcher />
@isset($user)
<div class="ms-1">{{ $user }}</div>
@endisset
@elseif (! $hasSidebar)
<x-theme-switcher />
@isset($user)
<div class="ms-1">{{ $user }}</div>
@endisset
@endif
</div>
</div>
</header>
@isset($header)
<div @class([
'border-b px-4 py-8 sm:px-6 lg:px-8',
'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800' => ! $transparentHeader,
'border-gray-200/70 bg-transparent dark:border-gray-800/70' => $transparentHeader,
])>
{{ $header }}
</div>
@endisset
<main id="main-content" @class([
'px-4 py-8 sm:px-6 sm:py-10 lg:px-8',
'flex flex-1 flex-col' => isset($footer),
])>
{{ $slot }}
</main>
@isset($footer)
<footer @class([
'border-t px-4 py-5 sm:px-6 lg:px-8',
'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800' => ! $transparentFooter,
'border-gray-200/70 bg-transparent dark:border-gray-800/70' => $transparentFooter,
])>
{{ $footer }}
</footer>
@endisset
</div>
</div>
</div>
resources/views/components/ui/app-shell/sidebar-footer.blade.php
@props([
'integrated' => false,
])
@php
/*
| Sidebar footer tray for app-shell sidebars. Groups the user, utility
| actions, theme colour picker and dark mode toggle in a compact grid.
*/
$rootClasses = $integrated
? 'shrink-0 border-t border-gray-200/60 px-3 pb-3 pt-3 dark:border-gray-800/60'
: 'shrink-0 border-t border-gray-200 p-3 dark:border-gray-800';
@endphp
<div {{ $attributes->merge([
'class' => $rootClasses,
'data-sidebar-footer-style' => $integrated ? 'integrated' : 'framed',
]) }}>
<div @class([
'overflow-visible',
'rounded-xl border border-gray-200/80 bg-gray-50/90 p-2 dark:border-gray-800/80 dark:bg-gray-900/50' => ! $integrated,
])>
@if (isset($user) || isset($sidebarFooter))
<div class="grid grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-2">
<div class="flex min-w-0 items-center">
{{ $user ?? '' }}
</div>
<div class="flex items-center justify-center">
{{ $sidebarFooter ?? '' }}
</div>
</div>
<div @class([
'border-t',
'my-2 border-gray-200/80 dark:border-gray-800/80' => ! $integrated,
'my-1.5 border-gray-200/60 dark:border-gray-800/60' => $integrated,
])></div>
@endif
<div class="relative grid grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-2">
<x-ui.theme-color-picker
class="min-w-0"
:full-width="true"
placement="top"
align="left"
/>
<x-ui.theme-mode-toggle />
</div>
</div>
</div>