This commit is contained in:
Bozhidar Slaveykov 2024-04-03 02:05:46 +03:00
parent 487f801a17
commit 83c0ef6e87
57 changed files with 2831 additions and 3 deletions

View file

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Website;
use Faker\Provider\Text;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class Websites extends BaseWidget
{
protected static bool $isLazy = false;
protected static ?string $heading = 'Last created websites';
protected int | string | array $columnSpan = 2;
public function table(Table $table): Table
{
return $table
->query(
Website::query()
)
->columns([
Tables\Columns\TextColumn::make('domain'),
Tables\Columns\TextColumn::make('hostingPlan.name'),
Tables\Columns\TextColumn::make('created_at')
]);
}
}

View file

@ -4,6 +4,7 @@ namespace App\Providers\Filament;
use App\Filament\Pages\Settings\Settings; use App\Filament\Pages\Settings\Settings;
use App\Filament\Widgets\CustomersCount; use App\Filament\Widgets\CustomersCount;
use App\Filament\Widgets\Websites;
use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Http\Middleware\DispatchServingFilamentEvent;
@ -33,6 +34,7 @@ class AdminPanelProvider extends PanelProvider
->id('admin') ->id('admin')
->path('admin') ->path('admin')
->login() ->login()
->sidebarWidth('14.5rem')
// ->brandLogo(fn () => view('filament.admin.logo')) // ->brandLogo(fn () => view('filament.admin.logo'))
->brandLogo(asset('images/phyre-logo.svg')) ->brandLogo(asset('images/phyre-logo.svg'))
->brandLogoHeight('2.5rem') ->brandLogoHeight('2.5rem')
@ -76,6 +78,7 @@ class AdminPanelProvider extends PanelProvider
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
CustomersCount::class, CustomersCount::class,
Websites::class,
// Widgets\AccountWidget::class, // Widgets\AccountWidget::class,
// Widgets\FilamentInfoWidget::class, // Widgets\FilamentInfoWidget::class,
]) ])

View file

@ -2,7 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
.fi-sidebar { /*.fi-sidebar {
width: 230px;
background-color: rgba(255, 255, 255, 0.07) !important; background-color: rgba(255, 255, 255, 0.07) !important;
} }*/

View file

@ -0,0 +1,13 @@
@props([
'tenant' => filament()->getTenant(),
])
<x-filament::avatar
:circular="false"
:src="filament()->getTenantAvatarUrl($tenant)"
:alt="__('filament-panels::layout.avatar.alt', ['name' => filament()->getTenantName($tenant)])"
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-tenant-avatar'])
"
/>

View file

@ -0,0 +1,12 @@
@props([
'user' => filament()->auth()->user(),
])
<x-filament::avatar
:src="filament()->getUserAvatarUrl($user)"
:alt="__('filament-panels::layout.avatar.alt', ['name' => filament()->getUserName($user)])"
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-user-avatar'])
"
/>

View file

@ -0,0 +1,34 @@
@props([
'actions',
'alignment' => null,
'fullWidth' => false,
])
@if (count($actions))
<div
@if ($this->areFormActionsSticky())
x-data="{
isSticky: false,
evaluatePageScrollPosition: function () {
this.isSticky =
document.body.scrollHeight >=
window.scrollY + window.innerHeight * 2
},
}"
x-init="evaluatePageScrollPosition"
x-on:scroll.window="evaluatePageScrollPosition"
x-bind:class="{
'fi-sticky sticky bottom-0 -mx-4 transform bg-white p-4 shadow-lg ring-1 ring-gray-950/5 transition dark:bg-gray-900 dark:ring-white/10 md:bottom-4 md:rounded-xl':
isSticky,
}"
@endif
class="fi-form-actions"
>
<x-filament::actions
:actions="$actions"
:alignment="$alignment ?? $this->getFormActionsAlignment()"
:full-width="$fullWidth"
/>
</div>
@endif

View file

@ -0,0 +1,14 @@
@props([
'method' => 'post',
])
<form
method="{{ $method }}"
x-data="{ isProcessing: false }"
x-on:submit="if (isProcessing) $event.preventDefault()"
x-on:form-processing-started="isProcessing = true"
x-on:form-processing-finished="isProcessing = false"
{{ $attributes->class(['fi-form grid gap-y-6']) }}
>
{{ $slot }}
</form>

View file

@ -0,0 +1,13 @@
@props([
'actions',
])
<div
{{ $attributes->class('fi-global-search-result-actions mt-3 flex gap-x-3 px-4 pb-4') }}
>
@foreach ($actions as $action)
@if ($action->isVisible())
{{ $action }}
@endif
@endforeach
</div>

View file

@ -0,0 +1,39 @@
@php
$debounce = filament()->getGlobalSearchDebounce();
$keyBindings = filament()->getGlobalSearchKeyBindings();
@endphp
<div
x-id="['input']"
{{ $attributes->class(['fi-global-search-field']) }}
>
<label x-bind:for="$id('input')" class="sr-only">
{{ __('filament-panels::global-search.field.label') }}
</label>
<x-filament::input.wrapper
inline-prefix
prefix-icon="heroicon-m-magnifying-glass"
prefix-icon-alias="panels::global-search.field"
wire:target="search"
>
<x-filament::input
autocomplete="off"
inline-prefix
:placeholder="__('filament-panels::global-search.field.placeholder')"
type="search"
wire:key="global-search.field.input"
x-bind:id="$id('input')"
x-on:keydown.down.prevent.stop="$dispatch('focus-first-global-search-result')"
x-data="{}"
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:model.live.debounce.' . $debounce => 'search',
'x-mousetrap.global.' . collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') => $keyBindings ? 'document.getElementById($id(\'input\')).focus()' : null,
])
)
"
/>
</x-filament::input.wrapper>
</div>

View file

@ -0,0 +1,19 @@
<div
x-data="{}"
x-on:focus-first-global-search-result.stop="$el.querySelector('.fi-global-search-result-link')?.focus()"
class="fi-global-search flex items-center"
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::GLOBAL_SEARCH_START) }}
<div class="sm:relative">
<x-filament-panels::global-search.field />
@if ($results !== null)
<x-filament-panels::global-search.results-container
:results="$results"
/>
@endif
</div>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::GLOBAL_SEARCH_END) }}
</div>

View file

@ -0,0 +1,5 @@
<p
{{ $attributes->class(['fi-global-search-no-results-message px-4 py-4 text-sm text-gray-500 dark:text-gray-400']) }}
>
{{ __('filament-panels::global-search.no_results_message') }}
</p>

View file

@ -0,0 +1,29 @@
@props([
'label',
'results',
])
<li
{{ $attributes->class(['fi-global-search-result-group']) }}
>
<div
class="sticky top-0 z-10 border-b border-gray-200 bg-gray-50 dark:border-white/10 dark:bg-gray-900"
>
<h3
class="px-4 py-2 text-sm font-semibold capitalize text-gray-950 dark:bg-white/5 dark:text-white"
>
{{ $label }}
</h3>
</div>
<ul class="divide-y divide-gray-200 dark:divide-white/10">
@foreach ($results as $result)
<x-filament-panels::global-search.result
:actions="$result->actions"
:details="$result->details"
:title="$result->title"
:url="$result->url"
/>
@endforeach
</ul>
</li>

View file

@ -0,0 +1,42 @@
@props([
'actions' => [],
'details' => [],
'title',
'url',
])
<li
{{ $attributes->class(['fi-global-search-result scroll-mt-9 transition duration-75 focus-within:bg-gray-50 hover:bg-gray-50 dark:focus-within:bg-white/5 dark:hover:bg-white/5']) }}
>
<a
{{ \Filament\Support\generate_href_html($url) }}
x-on:click="close()"
@class([
'fi-global-search-result-link block outline-none',
'pe-4 ps-4 pt-4' => $actions,
'p-4' => ! $actions,
])
>
<h4 class="text-sm font-medium text-gray-950 dark:text-white">
{{ $title }}
</h4>
@if ($details)
<dl class="mt-1">
@foreach ($details as $label => $value)
<div class="text-sm text-gray-500 dark:text-gray-400">
@if ($isAssoc ??= \Illuminate\Support\Arr::isAssoc($details))
<dt class="inline font-medium">{{ $label }}:</dt>
@endif
<dd class="inline">{{ $value }}</dd>
</div>
@endforeach
</dl>
@endif
</a>
@if ($actions)
<x-filament-panels::global-search.actions :actions="$actions" />
@endif
</li>

View file

@ -0,0 +1,50 @@
@props([
'results',
])
<div
x-data="{
isOpen: false,
open: function (event) {
this.isOpen = true
},
close: function (event) {
this.isOpen = false
},
}"
x-init="$nextTick(() => open())"
x-on:click.away="close()"
x-on:keydown.escape.window="close()"
x-on:keydown.up.prevent="$focus.wrap().previous()"
x-on:keydown.down.prevent="$focus.wrap().next()"
x-on:open-global-search-results.window="$nextTick(() => open())"
x-show="isOpen"
x-transition:enter-start="opacity-0"
x-transition:leave-end="opacity-0"
{{
$attributes->class([
'fi-global-search-results-ctn absolute inset-x-4 z-10 mt-2 max-h-96 overflow-auto rounded bg-white shadow-lg ring-1 ring-gray-950/5 transition dark:bg-gray-900 dark:ring-white/10 sm:inset-x-auto sm:end-0 sm:w-screen sm:max-w-sm',
// This zero translation along the z-axis fixes a Safari bug
// where the results container is incorrectly placed in the stacking context
// due to the overflow-x value of clip on the topbar element.
//
// https://github.com/filamentphp/filament/issues/8215
'[transform:translateZ(0)]',
])
}}
>
@if ($results->getCategories()->isEmpty())
<x-filament-panels::global-search.no-results-message />
@else
<ul class="divide-y divide-gray-200 dark:divide-white/10">
@foreach ($results->getCategories() as $group => $groupedResults)
<x-filament-panels::global-search.result-group
:label="$group"
:results="$groupedResults"
/>
@endforeach
</ul>
@endif
</div>

View file

@ -0,0 +1,47 @@
@props([
'actions' => [],
'breadcrumbs' => [],
'heading',
'subheading' => null,
])
<header
{{ $attributes->class(['fi-header flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between']) }}
>
<div>
@if ($breadcrumbs)
<x-filament::breadcrumbs
:breadcrumbs="$breadcrumbs"
class="mb-2 hidden sm:block"
/>
@endif
<h1
class="fi-header-heading text-2xl font-bold tracking-tight text-gray-950 dark:text-white sm:text-3xl"
>
{{ $heading }}
</h1>
@if ($subheading)
<p
class="fi-header-subheading mt-2 max-w-2xl text-lg text-gray-600 dark:text-gray-400"
>
{{ $subheading }}
</p>
@endif
</div>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_HEADER_ACTIONS_BEFORE, scopes: $this->getRenderHookScopes()) }}
@if ($actions)
<x-filament::actions
:actions="$actions"
@class([
'shrink-0',
'sm:mt-7' => $breadcrumbs,
])
/>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_HEADER_ACTIONS_AFTER, scopes: $this->getRenderHookScopes()) }}
</header>

View file

@ -0,0 +1,27 @@
@props([
'heading' => null,
'logo' => true,
'subheading' => null,
])
<header class="fi-simple-header flex flex-col items-center">
@if ($logo)
<x-filament-panels::logo class="mb-4" />
@endif
@if (filled($heading))
<h1
class="fi-simple-header-heading text-center text-2xl font-bold tracking-tight text-gray-950 dark:text-white"
>
{{ $heading }}
</h1>
@endif
@if (filled($subheading))
<p
class="fi-simple-header-subheading mt-2 text-center text-sm text-gray-500 dark:text-gray-400"
>
{{ $subheading }}
</p>
@endif
</header>

View file

@ -0,0 +1,129 @@
@props([
'livewire' => null,
])
<!DOCTYPE html>
<html
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
dir="{{ __('filament-panels::layout.direction') ?? 'ltr' }}"
@class([
'fi min-h-screen',
'dark' => filament()->hasDarkModeForced(),
])
>
<head>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_START, scopes: $livewire->getRenderHookScopes()) }}
<meta charset="utf-8" />
<meta name="csrf-token" content="{{ csrf_token() }}" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@if ($favicon = filament()->getFavicon())
<link rel="icon" href="{{ $favicon }}" />
@endif
<title>
{{ filled($title = strip_tags(($livewire ?? null)?->getTitle() ?? '')) ? "{$title} - " : null }}
{{ strip_tags(filament()->getBrandName()) }}
</title>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
<style>
[x-cloak=''],
[x-cloak='x-cloak'],
[x-cloak='1'] {
display: none !important;
}
@media (max-width: 1023px) {
[x-cloak='-lg'] {
display: none !important;
}
}
@media (min-width: 1024px) {
[x-cloak='lg'] {
display: none !important;
}
}
</style>
@filamentStyles
{{ filament()->getTheme()->getHtml() }}
{{ filament()->getFontHtml() }}
<style>
:root {
--font-family: '{!! filament()->getFontFamily() !!}';
--sidebar-width: {{ filament()->getSidebarWidth() }};
--collapsed-sidebar-width: {{ filament()->getCollapsedSidebarWidth() }};
--default-theme-mode: {{ filament()->getDefaultThemeMode()->value }};
}
</style>
@stack('styles')
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::STYLES_AFTER, scopes: $livewire->getRenderHookScopes()) }}
@if (! filament()->hasDarkMode())
<script>
localStorage.setItem('theme', 'light')
</script>
@elseif (filament()->hasDarkModeForced())
<script>
localStorage.setItem('theme', 'dark')
</script>
@else
<script>
const theme = localStorage.getItem('theme') ?? @js(filament()->getDefaultThemeMode()->value)
if (
theme === 'dark' ||
(theme === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)')
.matches)
) {
document.documentElement.classList.add('dark')
}
</script>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::HEAD_END, scopes: $livewire->getRenderHookScopes()) }}
</head>
<body
{{ $attributes
->merge(($livewire ?? null)?->getExtraBodyAttributes() ?? [], escape: false)
->class([
'fi-body',
'fi-panel-' . filament()->getId(),
'min-h-screen bg-gray-50 font-normal text-gray-950 antialiased dark:bg-gray-950 dark:text-white',
]) }}
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_START, scopes: $livewire->getRenderHookScopes()) }}
{{ $slot }}
@livewire(Filament\Livewire\Notifications::class)
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
@filamentScripts(withCore: true)
@if (config('filament.broadcasting.echo'))
<script data-navigate-once>
window.Echo = new window.EchoFactory(@js(config('filament.broadcasting.echo')))
window.dispatchEvent(new CustomEvent('EchoLoaded'))
</script>
@endif
@stack('scripts')
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SCRIPTS_AFTER, scopes: $livewire->getRenderHookScopes()) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::BODY_END, scopes: $livewire->getRenderHookScopes()) }}
</body>
</html>

View file

@ -0,0 +1,139 @@
@php
use Filament\Support\Enums\MaxWidth;
$navigation = filament()->getNavigation();
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
{{-- The sidebar is after the page content in the markup to fix issues with page content overlapping dropdown content from the sidebar. --}}
<div
class="fi-layout flex min-h-screen w-full flex-row-reverse overflow-x-clip"
>
<div
@if (filament()->isSidebarCollapsibleOnDesktop())
x-data="{}"
x-bind:class="{
'fi-main-ctn-sidebar-open': $store.sidebar.isOpen,
}"
x-bind:style="'display: flex; opacity:1;'" {{-- Mimics `x-cloak`, as using `x-cloak` causes visual issues with chart widgets --}}
@elseif (filament()->isSidebarFullyCollapsibleOnDesktop())
x-data="{}"
x-bind:class="{
'fi-main-ctn-sidebar-open': $store.sidebar.isOpen,
}"
x-bind:style="'display: flex; opacity:1;'" {{-- Mimics `x-cloak`, as using `x-cloak` causes visual issues with chart widgets --}}
@elseif (! (filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop() || filament()->hasTopNavigation() || (! filament()->hasNavigation())))
x-data="{}"
x-bind:style="'display: flex; opacity:1;'" {{-- Mimics `x-cloak`, as using `x-cloak` causes visual issues with chart widgets --}}
@endif
@class([
'fi-main-ctn w-screen flex-1 flex-col',
'h-full opacity-0 transition-all' => filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop(),
'opacity-0' => ! (filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop() || filament()->hasTopNavigation() || (! filament()->hasNavigation())),
'flex' => filament()->hasTopNavigation() || (! filament()->hasNavigation()),
])
>
@if (filament()->hasTopbar())
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_BEFORE, scopes: $livewire->getRenderHookScopes()) }}
<x-filament-panels::topbar :navigation="$navigation" />
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_AFTER, scopes: $livewire->getRenderHookScopes()) }}
@endif
<main
@class([
'fi-main mx-auto h-full w-full px-4 md:px-6 lg:px-8',
match ($maxContentWidth ??= (filament()->getMaxContentWidth() ?? MaxWidth::SevenExtraLarge)) {
MaxWidth::ExtraSmall, 'xs' => 'max-w-xs',
MaxWidth::Small, 'sm' => 'max-w-sm',
MaxWidth::Medium, 'md' => 'max-w-md',
MaxWidth::Large, 'lg' => 'max-w-lg',
MaxWidth::ExtraLarge, 'xl' => 'max-w-xl',
MaxWidth::TwoExtraLarge, '2xl' => 'max-w-2xl',
MaxWidth::ThreeExtraLarge, '3xl' => 'max-w-3xl',
MaxWidth::FourExtraLarge, '4xl' => 'max-w-4xl',
MaxWidth::FiveExtraLarge, '5xl' => 'max-w-5xl',
MaxWidth::SixExtraLarge, '6xl' => 'max-w-6xl',
MaxWidth::SevenExtraLarge, '7xl' => 'max-w-7xl',
MaxWidth::Full, 'full' => 'max-w-full',
MaxWidth::MinContent, 'min' => 'max-w-min',
MaxWidth::MaxContent, 'max' => 'max-w-max',
MaxWidth::FitContent, 'fit' => 'max-w-fit',
MaxWidth::Prose, 'prose' => 'max-w-prose',
MaxWidth::ScreenSmall, 'screen-sm' => 'max-w-screen-sm',
MaxWidth::ScreenMedium, 'screen-md' => 'max-w-screen-md',
MaxWidth::ScreenLarge, 'screen-lg' => 'max-w-screen-lg',
MaxWidth::ScreenExtraLarge, 'screen-xl' => 'max-w-screen-xl',
MaxWidth::ScreenTwoExtraLarge, 'screen-2xl' => 'max-w-screen-2xl',
default => $maxContentWidth,
},
])
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_START, scopes: $livewire->getRenderHookScopes()) }}
{{ $slot }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::CONTENT_END, scopes: $livewire->getRenderHookScopes()) }}
</main>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
</div>
@if (filament()->hasNavigation())
<div
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
x-transition.opacity.300ms
class="fi-sidebar-close-overlay fixed inset-0 z-30 bg-gray-950/50 transition duration-500 dark:bg-gray-950/75 lg:hidden"
></div>
<x-filament-panels::sidebar
:navigation="$navigation"
class="fi-main-sidebar"
/>
<script>
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
let activeSidebarItem = document.querySelector(
'.fi-main-sidebar .fi-sidebar-item.fi-active',
)
if (
!activeSidebarItem ||
activeSidebarItem.offsetParent === null
) {
activeSidebarItem = document.querySelector(
'.fi-main-sidebar .fi-sidebar-group.fi-active',
)
}
if (
!activeSidebarItem ||
activeSidebarItem.offsetParent === null
) {
return
}
const sidebarWrapper = document.querySelector(
'.fi-main-sidebar .fi-sidebar-nav',
)
if (!sidebarWrapper) {
return
}
sidebarWrapper.scrollTo(
0,
activeSidebarItem.offsetTop -
window.innerHeight / 2,
)
}, 0)
})
</script>
@endif
</div>
</x-filament-panels::layout.base>

View file

@ -0,0 +1,52 @@
@php
use Filament\Support\Enums\MaxWidth;
@endphp
<x-filament-panels::layout.base :livewire="$livewire">
@props([
'after' => null,
'heading' => null,
'subheading' => null,
])
<div class="fi-simple-layout flex min-h-screen flex-col items-center">
@if (($hasTopbar ?? true) && filament()->auth()->check())
<div
class="absolute end-0 top-0 flex h-16 items-center gap-x-4 pe-4 md:pe-6 lg:pe-8"
>
@if (filament()->hasDatabaseNotifications())
@livewire(Filament\Livewire\DatabaseNotifications::class, ['lazy' => true])
@endif
<x-filament-panels::user-menu />
</div>
@endif
<div
class="fi-simple-main-ctn flex w-full flex-grow items-center justify-center"
>
<main
@class([
'fi-simple-main my-16 w-full bg-white px-6 py-12 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 sm:rounded-xl sm:px-12',
match ($maxWidth ?? null) {
MaxWidth::ExtraSmall, 'xs' => 'sm:max-w-xs',
MaxWidth::Small, 'sm' => 'sm:max-w-sm',
MaxWidth::Medium, 'md' => 'sm:max-w-md',
MaxWidth::ExtraLarge, 'xl' => 'sm:max-w-xl',
MaxWidth::TwoExtraLarge, '2xl' => 'sm:max-w-2xl',
MaxWidth::ThreeExtraLarge, '3xl' => 'sm:max-w-3xl',
MaxWidth::FourExtraLarge, '4xl' => 'sm:max-w-4xl',
MaxWidth::FiveExtraLarge, '5xl' => 'sm:max-w-5xl',
MaxWidth::SixExtraLarge, '6xl' => 'sm:max-w-6xl',
MaxWidth::SevenExtraLarge, '7xl' => 'sm:max-w-7xl',
default => 'sm:max-w-lg',
},
])
>
{{ $slot }}
</main>
</div>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::FOOTER, scopes: $livewire->getRenderHookScopes()) }}
</div>
</x-filament-panels::layout.base>

View file

@ -0,0 +1,57 @@
@php
$brandName = filament()->getBrandName();
$brandLogo = filament()->getBrandLogo();
$brandLogoHeight = filament()->getBrandLogoHeight() ?? '1.5rem';
$darkModeBrandLogo = filament()->getDarkModeBrandLogo();
$hasDarkModeBrandLogo = filled($darkModeBrandLogo);
$getLogoClasses = fn (bool $isDarkMode): string => \Illuminate\Support\Arr::toCssClasses([
'fi-logo',
'flex' => ! $hasDarkModeBrandLogo,
'flex dark:hidden' => $hasDarkModeBrandLogo && (! $isDarkMode),
'hidden dark:flex' => $hasDarkModeBrandLogo && $isDarkMode,
]);
$logoStyles = "height: {$brandLogoHeight}";
@endphp
@capture($content, $logo, $isDarkMode = false)
@if ($logo instanceof \Illuminate\Contracts\Support\Htmlable)
<div
{{
$attributes
->class([$getLogoClasses($isDarkMode)])
->style([$logoStyles])
}}
>
{{ $logo }}
</div>
@elseif (filled($logo))
<img
alt="{{ __('filament-panels::layout.logo.alt', ['name' => $brandName]) }}"
src="{{ $logo }}"
{{
$attributes
->class([$getLogoClasses($isDarkMode)])
->style([$logoStyles])
}}
/>
@else
<div
{{
$attributes->class([
$getLogoClasses($isDarkMode),
'text-xl font-bold leading-5 tracking-tight text-gray-950 dark:text-white',
])
}}
>
{{ $brandName }}
</div>
@endif
@endcapture
{{ $content($brandLogo) }}
@if ($hasDarkModeBrandLogo)
{{ $content($darkModeBrandLogo, isDarkMode: true) }}
@endif

View file

@ -0,0 +1,148 @@
@props([
'fullHeight' => false,
])
@php
use Filament\Pages\SubNavigationPosition;
$subNavigation = $this->getCachedSubNavigation();
$subNavigationPosition = $this->getSubNavigationPosition();
$widgetData = $this->getWidgetData();
@endphp
<div
{{
$attributes->class([
'fi-page',
'h-full' => $fullHeight,
])
}}
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_START, scopes: $this->getRenderHookScopes()) }}
<section
@class([
'flex flex-col gap-y-8 py-8',
'h-full' => $fullHeight,
])
>
@if ($header = $this->getHeader())
{{ $header }}
@elseif ($heading = $this->getHeading())
@php
$subheading = $this->getSubheading();
@endphp
<x-filament-panels::header
:actions="$this->getCachedHeaderActions()"
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
:heading="$heading"
:subheading="$subheading"
>
@if ($heading instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="heading">
{{ $heading }}
</x-slot>
@endif
@if ($subheading instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="subheading">
{{ $subheading }}
</x-slot>
@endif
</x-filament-panels::header>
@endif
<div
@class([
'flex flex-col gap-8' => $subNavigation,
match ($subNavigationPosition) {
SubNavigationPosition::Start, SubNavigationPosition::End => 'md:flex-row',
default => null,
} => $subNavigation,
'h-full' => $fullHeight,
])
>
@if ($subNavigation)
<x-filament-panels::page.sub-navigation.select
:navigation="$subNavigation"
/>
@if ($subNavigationPosition === SubNavigationPosition::Start)
<x-filament-panels::page.sub-navigation.sidebar
:navigation="$subNavigation"
/>
@endif
@if ($subNavigationPosition === SubNavigationPosition::Top)
<x-filament-panels::page.sub-navigation.tabs
:navigation="$subNavigation"
/>
@endif
@endif
<div
@class([
'grid flex-1 auto-cols-fr gap-y-8',
'h-full' => $fullHeight,
])
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_HEADER_WIDGETS_BEFORE, scopes: $this->getRenderHookScopes()) }}
@if ($headerWidgets = $this->getVisibleHeaderWidgets())
<x-filament-widgets::widgets
:columns="$this->getHeaderWidgetsColumns()"
:data="$widgetData"
:widgets="$headerWidgets"
class="fi-page-header-widgets"
/>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_HEADER_WIDGETS_AFTER, scopes: $this->getRenderHookScopes()) }}
{{ $slot }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_FOOTER_WIDGETS_BEFORE, scopes: $this->getRenderHookScopes()) }}
@if ($footerWidgets = $this->getVisibleFooterWidgets())
<x-filament-widgets::widgets
:columns="$this->getFooterWidgetsColumns()"
:data="$widgetData"
:widgets="$footerWidgets"
class="fi-page-footer-widgets"
/>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_FOOTER_WIDGETS_AFTER, scopes: $this->getRenderHookScopes()) }}
</div>
@if ($subNavigation && $subNavigationPosition === SubNavigationPosition::End)
<x-filament-panels::page.sub-navigation.sidebar
:navigation="$subNavigation"
/>
@endif
</div>
@if ($footer = $this->getFooter())
{{ $footer }}
@endif
</section>
@if (! ($this instanceof \Filament\Tables\Contracts\HasTable))
<x-filament-actions::modals />
@elseif ($this->isTableLoaded() && filled($this->defaultTableAction))
<div
wire:init="mountTableAction(@js($this->defaultTableAction), @if (filled($this->defaultTableActionRecord)) @js($this->defaultTableActionRecord) @else {{ 'null' }} @endif @if (filled($this->defaultTableActionArguments)) , @js($this->defaultTableActionArguments) @endif)"
></div>
@endif
@if (filled($this->defaultAction))
<div
wire:init="mountAction(@js($this->defaultAction) @if (filled($this->defaultActionArguments)) , @js($this->defaultActionArguments) @endif)"
></div>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::PAGE_END, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::unsaved-action-changes-alert />
</div>

View file

@ -0,0 +1,20 @@
@props([
'heading' => null,
'subheading' => null,
])
<div {{ $attributes->class(['fi-simple-page']) }}>
<section class="grid auto-cols-fr gap-y-6">
<x-filament-panels::header.simple
:heading="$heading ??= $this->getHeading()"
:logo="$this->hasLogo()"
:subheading="$subheading ??= $this->getSubHeading()"
/>
{{ $slot }}
</section>
@if (! $this instanceof \Filament\Tables\Contracts\HasTable)
<x-filament-actions::modals />
@endif
</div>

View file

@ -0,0 +1,43 @@
@props([
'navigation',
])
<x-filament::input.wrapper
wire:ignore
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-page-sub-navigation-select md:hidden'])
"
>
<x-filament::input.select
x-data="{}"
x-on:change="window.location.href = $event.target.value"
>
@foreach ($navigation as $navigationGroup)
@capture($options)
@foreach ($navigationGroup->getItems() as $navigationItem)
@foreach ([$navigationItem, ...$navigationItem->getChildItems()] as $navigationItemChild)
<option
@selected($navigationItemChild->isActive())
value="{{ $navigationItemChild->getUrl() }}"
>
@if ($loop->index)
&ensp;&ensp;
@endif
{{ $navigationItemChild->getLabel() }}
</option>
@endforeach
@endforeach
@endcapture
@if (filled($navigationGroupLabel = $navigationGroup->getLabel()))
<optgroup label="{{ $navigationGroupLabel }}">
{{ $options() }}
</optgroup>
@else
{{ $options() }}
@endif
@endforeach
</x-filament::input.select>
</x-filament::input.wrapper>

View file

@ -0,0 +1,21 @@
@props([
'navigation',
])
<ul
wire:ignore
{{ $attributes->class(['fi-page-sub-navigation-sidebar hidden w-72 flex-col gap-y-7 md:flex']) }}
>
@foreach ($navigation as $navigationGroup)
<x-filament-panels::sidebar.group
:active="$navigationGroup->isActive()"
:collapsible="$navigationGroup->isCollapsible()"
:icon="$navigationGroup->getIcon()"
:items="$navigationGroup->getItems()"
:label="$navigationGroup->getLabel()"
:sidebar-collapsible="false"
sub-navigation
:attributes="\Filament\Support\prepare_inherited_attributes($navigationGroup->getExtraSidebarAttributeBag())"
/>
@endforeach
</ul>

View file

@ -0,0 +1,77 @@
@props([
'navigation',
])
<x-filament::tabs
wire:ignore
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-page-sub-navigation-tabs hidden md:flex'])
"
>
@foreach ($navigation as $navigationGroup)
@if ($navigationGroupLabel = $navigationGroup->getLabel())
<x-filament::dropdown placement="bottom-start">
<x-slot name="trigger">
<x-filament::tabs.item
:active="$navigationGroup->isActive()"
:icon="$navigationGroup->getIcon()"
>
{{ $navigationGroupLabel }}
</x-filament::tabs.item>
</x-slot>
<x-filament::dropdown.list>
@foreach ($navigationGroup->getItems() as $navigationItem)
@php
$navigationItemIcon = $navigationItem->getIcon();
$navigationItemIcon = $navigationItem->isActive() ? ($navigationItem->getActiveIcon() ?? $navigationItemIcon) : $navigationItemIcon;
@endphp
<x-filament::dropdown.list.item
:badge="$navigationItem->getBadge()"
:badge-color="$navigationItem->getBadgeColor()"
:href="$navigationItem->getUrl()"
:icon="$navigationItemIcon"
tag="a"
:target="$navigationItem->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $navigationItem->getLabel() }}
@if ($navigationItemIcon instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="icon">
{{ $navigationItemIcon }}
</x-slot>
@endif
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>
@else
@foreach ($navigationGroup->getItems() as $navigationItem)
@php
$navigationItemIcon = $navigationItem->getIcon();
$navigationItemIcon = $navigationItem->isActive() ? ($navigationItem->getActiveIcon() ?? $navigationItemIcon) : $navigationItemIcon;
@endphp
<x-filament::tabs.item
:active="$navigationItem->isActive()"
:badge="$navigationItem->getBadge()"
:badge-color="$navigationItem->getBadgeColor()"
:href="$navigationItem->getUrl()"
:icon="$navigationItemIcon"
tag="a"
:target="$navigationItem->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $navigationItem->getLabel() }}
@if ($navigationItemIcon instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="icon">
{{ $navigationItemIcon }}
</x-slot>
@endif
</x-filament::tabs.item>
@endforeach
@endif
@endforeach
</x-filament::tabs>

View file

@ -0,0 +1,23 @@
@php
use Filament\Support\Facades\FilamentView;
@endphp
@if ($this->hasUnsavedDataChangesAlert() && (! FilamentView::hasSpaMode()))
@script
<script>
window.addEventListener('beforeunload', (event) => {
if (
window.jsMd5(
JSON.stringify($wire.data).replace(/\\/g, ''),
) === $wire.savedDataHash ||
$wire?.__instance?.effects?.redirect
) {
return
}
event.preventDefault()
event.returnValue = true
})
</script>
@endscript
@endif

View file

@ -0,0 +1,111 @@
@props([
'activeLocale' => null,
'activeManager',
'content' => null,
'contentTabLabel' => null,
'managers',
'ownerRecord',
'pageClass',
])
<div class="fi-resource-relation-managers flex flex-col gap-y-6">
@php
$activeManager = strval($activeManager);
$normalizeRelationManagerClass = function (string | Filament\Resources\RelationManagers\RelationManagerConfiguration $manager): string {
if ($manager instanceof \Filament\Resources\RelationManagers\RelationManagerConfiguration) {
return $manager->relationManager;
}
return $manager;
};
@endphp
@if ((count($managers) > 1) || $content)
<x-filament::tabs>
@php
$tabs = $managers;
if ($content) {
$tabs = array_replace([null => null], $tabs);
}
@endphp
@foreach ($tabs as $tabKey => $manager)
@php
$tabKey = strval($tabKey);
$isGroup = $manager instanceof \Filament\Resources\RelationManagers\RelationGroup;
if ($isGroup) {
$manager->ownerRecord($ownerRecord);
$manager->pageClass($pageClass);
} elseif (filled($tabKey)) {
$manager = $normalizeRelationManagerClass($manager);
}
@endphp
<x-filament::tabs.item
:active="$activeManager === $tabKey"
:badge="filled($tabKey) ? ($isGroup ? $manager->getBadge() : $manager::getBadge($ownerRecord, $pageClass)) : null"
:badge-color="filled($tabKey) ? ($isGroup ? $manager->getBadgeColor() : $manager::getBadgeColor($ownerRecord, $pageClass)) : null"
:badge-tooltip="filled($tabKey) ? ($isGroup ? $manager->getBadgeTooltip() : $manager::getBadgeTooltip($ownerRecord, $pageClass)) : null"
:icon="filled($tabKey) ? ($isGroup ? $manager->getIcon() : $manager::getIcon($ownerRecord, $pageClass)) : null"
:icon-position="filled($tabKey) ? ($isGroup ? $manager->getIconPosition() : $manager::getIconPosition($ownerRecord, $pageClass)) : null"
:wire:click="'$set(\'activeRelationManager\', ' . (filled($tabKey) ? ('\'' . $tabKey . '\'') : 'null') . ')'"
>
@if (filled($tabKey))
{{ $isGroup ? $manager->getLabel() : $manager::getTitle($ownerRecord, $pageClass) }}
@elseif ($content)
{{ $contentTabLabel }}
@endif
</x-filament::tabs.item>
@endforeach
</x-filament::tabs>
@endif
@if (filled($activeManager) && isset($managers[$activeManager]))
<div
@if (count($managers) > 1)
id="relationManager{{ ucfirst($activeManager) }}"
role="tabpanel"
tabindex="0"
@endif
wire:key="{{ $this->getId() }}.relation-managers.active"
class="flex flex-col gap-y-4"
>
@php
$managerLivewireProperties = ['ownerRecord' => $ownerRecord, 'pageClass' => $pageClass];
if (filled($activeLocale)) {
$managerLivewireProperties['activeLocale'] = $activeLocale;
}
@endphp
@if ($managers[$activeManager] instanceof \Filament\Resources\RelationManagers\RelationGroup)
@foreach ($managers[$activeManager]->ownerRecord($ownerRecord)->pageClass($pageClass)->getManagers() as $groupedManagerKey => $groupedManager)
@php
$normalizedGroupedManagerClass = $normalizeRelationManagerClass($groupedManager);
@endphp
@livewire(
$normalizedGroupedManagerClass,
[...$managerLivewireProperties, ...(($groupedManager instanceof \Filament\Resources\RelationManagers\RelationManagerConfiguration) ? [...$groupedManager->relationManager::getDefaultProperties(), ...$groupedManager->getProperties()] : $groupedManager::getDefaultProperties())],
key("{$normalizedGroupedManagerClass}-{$groupedManagerKey}"),
)
@endforeach
@else
@php
$manager = $managers[$activeManager];
$normalizedManagerClass = $normalizeRelationManagerClass($manager);
@endphp
@livewire(
$normalizedManagerClass,
[...$managerLivewireProperties, ...(($manager instanceof \Filament\Resources\RelationManagers\RelationManagerConfiguration) ? [...$manager->relationManager::getDefaultProperties(), ...$manager->getProperties()] : $manager::getDefaultProperties())],
key($normalizedManagerClass),
)
@endif
</div>
@elseif ($content)
{{ $content }}
@endif
</div>

View file

@ -0,0 +1,32 @@
@if (count($tabs = $this->getCachedTabs()))
@php
$activeTab = strval($this->activeTab);
$renderHookScopes = $this->getRenderHookScopes();
@endphp
<x-filament::tabs>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_TABS_START, scopes: $renderHookScopes) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABS_START, scopes: $renderHookScopes) }}
@foreach ($tabs as $tabKey => $tab)
@php
$tabKey = strval($tabKey);
@endphp
<x-filament::tabs.item
:active="$activeTab === $tabKey"
:badge="$tab->getBadge()"
:badge-color="$tab->getBadgeColor()"
:icon="$tab->getIcon()"
:icon-position="$tab->getIconPosition()"
:wire:click="'$set(\'activeTab\', ' . (filled($tabKey) ? ('\'' . $tabKey . '\'') : 'null') . ')'"
:attributes="$tab->getExtraAttributeBag()"
>
{{ $tab->getLabel() ?? $this->generateTabLabel($tabKey) }}
</x-filament::tabs.item>
@endforeach
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_TABS_END, scopes: $renderHookScopes) }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABS_END, scopes: $renderHookScopes) }}
</x-filament::tabs>
@endif

View file

@ -0,0 +1,224 @@
@props([
'active' => false,
'collapsible' => true,
'icon' => null,
'items' => [],
'label' => null,
'sidebarCollapsible' => true,
'subNavigation' => false,
])
@php
$sidebarCollapsible = $sidebarCollapsible && filament()->isSidebarCollapsibleOnDesktop();
$hasDropdown = filled($label) && filled($icon) && $sidebarCollapsible;
@endphp
<li
x-data="{ label: @js($subNavigation ? "sub_navigation_{$label}" : $label) }"
data-group-label="{{ $label }}"
{{
$attributes->class([
'fi-sidebar-group flex flex-col gap-y-1',
'fi-active' => $active,
])
}}
>
@if ($label)
<div
@if ($collapsible)
x-on:click="$store.sidebar.toggleCollapsedGroup(label)"
@endif
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen"
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
@class([
'fi-sidebar-group-button flex items-center gap-x-3 px-2 py-2',
'cursor-pointer' => $collapsible,
])
>
@if ($icon)
<x-filament::icon
:icon="$icon"
class="fi-sidebar-group-icon h-6 w-6 text-gray-400 dark:text-gray-500"
/>
@endif
<span
class="fi-sidebar-group-label flex-1 text-sm font-medium leading-6 text-gray-500 dark:text-gray-400"
>
{{ $label }}
</span>
@if ($collapsible)
<x-filament::icon-button
color="gray"
icon="heroicon-m-chevron-up"
icon-alias="panels::sidebar.group.collapse-button"
:label="$label"
x-bind:aria-expanded="! $store.sidebar.groupIsCollapsed(label)"
x-on:click.stop="$store.sidebar.toggleCollapsedGroup(label)"
class="fi-sidebar-group-collapse-button"
x-bind:class="{ '-rotate-180': $store.sidebar.groupIsCollapsed(label) }"
/>
@endif
</div>
@endif
@if ($hasDropdown)
<x-filament::dropdown
:placement="(__('filament-panels::layout.direction') === 'rtl') ? 'left-start' : 'right-start'"
teleport
x-show="! $store.sidebar.isOpen"
>
<x-slot name="trigger">
<button
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($label),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
class="relative flex flex-1 items-center justify-center gap-x-3 rounded px-2 py-2 outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<x-filament::icon
:icon="$icon"
@class([
'h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
</button>
</x-slot>
@php
$lists = [];
foreach ($items as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@if (filled($label))
<x-filament::dropdown.header>
{{ $label }}
</x-filament::dropdown.header>
@endif
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@endif
<ul
@if (filled($label))
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen ? ! $store.sidebar.groupIsCollapsed(label) : ! @js($hasDropdown)"
@else
x-show="! $store.sidebar.groupIsCollapsed(label)"
@endif
x-collapse.duration.200ms
@endif
@if ($sidebarCollapsible)
x-transition:enter="delay-100 lg:transition"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
class="fi-sidebar-group-items flex flex-col gap-y-1"
>
@foreach ($items as $item)
@php
$itemIcon = $item->getIcon();
$itemActiveIcon = $item->getActiveIcon();
if ($icon) {
if ($hasDropdown || (blank($itemIcon) && blank($itemActiveIcon))) {
$itemIcon = null;
$itemActiveIcon = null;
} else {
throw new \Exception('Navigation group [' . $label . '] has an icon but one or more of its items also have icons. Either the group or its items can have icons, but not both. This is to ensure a proper user experience.');
}
}
@endphp
<x-filament-panels::sidebar.item
:active="$item->isActive()"
:active-child-items="$item->isChildItemsActive()"
:active-icon="$itemActiveIcon"
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:child-items="$item->getChildItems()"
:first="$loop->first"
:grouped="filled($label)"
:icon="$itemIcon"
:last="$loop->last"
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
:sidebar-collapsible="$sidebarCollapsible"
:url="$item->getUrl()"
>
{{ $item->getLabel() }}
@if ($itemIcon instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="icon">
{{ $itemIcon }}
</x-slot>
@endif
@if ($itemActiveIcon instanceof \Illuminate\Contracts\Support\Htmlable)
<x-slot name="activeIcon">
{{ $itemActiveIcon }}
</x-slot>
@endif
</x-filament-panels::sidebar.item>
@endforeach
</ul>
</li>

View file

@ -0,0 +1,179 @@
@props([
'navigation',
])
@php
$openSidebarClasses = 'fi-sidebar-open w-[--sidebar-width] translate-x-0 shadow-xl ring-1 ring-gray-950/5 dark:ring-white/10 rtl:-translate-x-0';
$isRtl = __('filament-panels::layout.direction') === 'rtl';
@endphp
{{-- format-ignore-start --}}
<aside
x-data="{}"
@if (filament()->isSidebarCollapsibleOnDesktop() && (! filament()->hasTopNavigation()))
x-cloak
x-bind:class="
$store.sidebar.isOpen
? @js($openSidebarClasses . ' ' . 'lg:sticky')
: '-translate-x-full rtl:translate-x-full lg:sticky lg:translate-x-0 rtl:lg:-translate-x-0'
"
@else
@if (filament()->hasTopNavigation())
x-cloak
x-bind:class="$store.sidebar.isOpen ? @js($openSidebarClasses) : '-translate-x-full rtl:translate-x-full'"
@elseif (filament()->isSidebarFullyCollapsibleOnDesktop())
x-cloak
x-bind:class="$store.sidebar.isOpen ? @js($openSidebarClasses . ' ' . 'lg:sticky') : '-translate-x-full rtl:translate-x-full'"
@else
x-cloak="-lg"
x-bind:class="
$store.sidebar.isOpen
? @js($openSidebarClasses . ' ' . 'lg:sticky')
: 'w-[--sidebar-width] -translate-x-full rtl:translate-x-full lg:sticky'
"
@endif
@endif
{{
$attributes->class([
'fi-sidebar fixed inset-y-0 start-0 z-30 flex flex-col h-screen content-start bg-white transition-all dark:bg-gray-900 lg:z-0 lg:bg-transparent lg:shadow-none lg:ring-0 lg:transition-none dark:lg:bg-transparent',
'lg:translate-x-0 rtl:lg:-translate-x-0' => ! (filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop() || filament()->hasTopNavigation()),
'lg:-translate-x-full rtl:lg:translate-x-full' => filament()->hasTopNavigation(),
])
}}
>
<div class="overflow-x-clip">
<header
class="fi-sidebar-header flex h-16 items-center bg-white px-6 ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 lg:shadow-sm"
>
<div
@if (filament()->isSidebarCollapsibleOnDesktop())
x-show="$store.sidebar.isOpen"
x-transition:enter="lg:transition lg:delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
>
@if ($homeUrl = filament()->getHomeUrl())
<a {{ \Filament\Support\generate_href_html($homeUrl) }}>
<x-filament-panels::logo />
</a>
@else
<x-filament-panels::logo />
@endif
</div>
@if (filament()->isSidebarCollapsibleOnDesktop())
<x-filament::icon-button
color="gray"
:icon="$isRtl ? 'heroicon-o-chevron-left' : 'heroicon-o-chevron-right'"
{{-- @deprecated Use `panels::sidebar.expand-button.rtl` instead of `panels::sidebar.expand-button` for RTL. --}}
:icon-alias="$isRtl ? ['panels::sidebar.expand-button.rtl', 'panels::sidebar.expand-button'] : 'panels::sidebar.expand-button'"
icon-size="lg"
:label="__('filament-panels::layout.actions.sidebar.expand.label')"
x-cloak
x-data="{}"
x-on:click="$store.sidebar.open()"
x-show="! $store.sidebar.isOpen"
class="mx-auto"
/>
@endif
@if (filament()->isSidebarCollapsibleOnDesktop() || filament()->isSidebarFullyCollapsibleOnDesktop())
<x-filament::icon-button
color="gray"
:icon="$isRtl ? 'heroicon-o-chevron-right' : 'heroicon-o-chevron-left'"
{{-- @deprecated Use `panels::sidebar.collapse-button.rtl` instead of `panels::sidebar.collapse-button` for RTL. --}}
:icon-alias="$isRtl ? ['panels::sidebar.collapse-button.rtl', 'panels::sidebar.collapse-button'] : 'panels::sidebar.collapse-button'"
icon-size="lg"
:label="__('filament-panels::layout.actions.sidebar.collapse.label')"
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
class="ms-auto hidden lg:flex"
/>
@endif
</header>
</div>
<nav
class="fi-sidebar-nav bg-white/5 flex-grow flex flex-col gap-y-7 overflow-y-auto overflow-x-hidden px-6 py-8"
style="scrollbar-gutter: stable"
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIDEBAR_NAV_START) }}
@if (filament()->hasTenancy() && filament()->hasTenantMenu())
<div
@class([
'fi-sidebar-nav-tenant-menu-ctn',
'-mx-2' => ! filament()->isSidebarCollapsibleOnDesktop(),
])
@if (filament()->isSidebarCollapsibleOnDesktop())
x-bind:class="$store.sidebar.isOpen ? '-mx-2' : '-mx-4'"
@endif
>
<x-filament-panels::tenant-menu />
</div>
@endif
<ul class="fi-sidebar-nav-groups -mx-2 flex flex-col gap-y-7">
@foreach ($navigation as $group)
<x-filament-panels::sidebar.group
:active="$group->isActive()"
:collapsible="$group->isCollapsible()"
:icon="$group->getIcon()"
:items="$group->getItems()"
:label="$group->getLabel()"
:attributes="\Filament\Support\prepare_inherited_attributes($group->getExtraSidebarAttributeBag())"
/>
@endforeach
</ul>
<script>
var collapsedGroups = JSON.parse(
localStorage.getItem('collapsedGroups'),
)
if (collapsedGroups === null || collapsedGroups === 'null') {
localStorage.setItem(
'collapsedGroups',
JSON.stringify(@js(
collect($navigation)
->filter(fn (\Filament\Navigation\NavigationGroup $group): bool => $group->isCollapsed())
->map(fn (\Filament\Navigation\NavigationGroup $group): string => $group->getLabel())
->values()
)),
)
}
collapsedGroups = JSON.parse(
localStorage.getItem('collapsedGroups'),
)
document
.querySelectorAll('.fi-sidebar-group')
.forEach((group) => {
if (
!collapsedGroups.includes(group.dataset.groupLabel)
) {
return
}
// Alpine.js loads too slow, so attempt to hide a
// collapsed sidebar group earlier.
group.querySelector(
'.fi-sidebar-group-items',
).style.display = 'none'
group
.querySelector('.fi-sidebar-group-collapse-button')
.classList.add('rotate-180')
})
</script>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIDEBAR_NAV_END) }}
</nav>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::SIDEBAR_FOOTER) }}
</aside>
{{-- format-ignore-end --}}

View file

@ -0,0 +1,154 @@
@props([
'active' => false,
'activeChildItems' => false,
'activeIcon' => null,
'badge' => null,
'badgeColor' => null,
'badgeTooltip' => null,
'childItems' => [],
'first' => false,
'grouped' => false,
'icon' => null,
'last' => false,
'shouldOpenUrlInNewTab' => false,
'sidebarCollapsible' => true,
'subGrouped' => false,
'url',
])
@php
$sidebarCollapsible = $sidebarCollapsible && filament()->isSidebarCollapsibleOnDesktop();
@endphp
<li
{{
$attributes->class([
'fi-sidebar-item',
// @deprecated `fi-sidebar-item-active` has been replaced by `fi-active`.
'fi-active fi-sidebar-item-active' => $active,
'flex flex-col gap-y-1' => $active || $activeChildItems,
])
}}
>
<a
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
x-on:click="window.matchMedia(`(max-width: 1024px)`).matches && $store.sidebar.close()"
@if ($sidebarCollapsible)
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($slot->toHtml()),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
@endif
@class([
'fi-sidebar-item-button relative flex items-center justify-center gap-x-3 rounded px-2 py-2 outline-none transition duration-75',
'hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5' => filled($url),
'bg-gray-100 dark:bg-white/5' => $active,
])
>
@if (filled($icon) && ((! $subGrouped) || $sidebarCollapsible))
<x-filament::icon
:icon="($active && $activeIcon) ? $activeIcon : $icon"
:x-show="($subGrouped && $sidebarCollapsible) ? '! $store.sidebar.isOpen' : false"
@class([
'fi-sidebar-item-icon h-6 w-6',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
@endif
@if ((blank($icon) && $grouped) || $subGrouped)
<div
@if (filled($icon) && $subGrouped && $sidebarCollapsible)
x-show="$store.sidebar.isOpen"
@endif
class="fi-sidebar-item-grouped-border relative flex h-6 w-6 items-center justify-center"
>
@if (! $first)
<div
class="absolute -top-1/2 bottom-1/2 w-px bg-gray-300 dark:bg-gray-600"
></div>
@endif
@if (! $last)
<div
class="absolute -bottom-1/2 top-1/2 w-px bg-gray-300 dark:bg-gray-600"
></div>
@endif
<div
@class([
'relative h-1.5 w-1.5 rounded-full',
'bg-gray-400 dark:bg-gray-500' => ! $active,
'bg-primary-600 dark:bg-primary-400' => $active,
])
></div>
</div>
@endif
<span
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen"
x-transition:enter="lg:transition lg:delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
@class([
'fi-sidebar-item-label flex-1 truncate text-sm font-medium',
'text-gray-700 dark:text-gray-200' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
>
{{ $slot }}
</span>
@if (filled($badge))
<span
@if ($sidebarCollapsible)
x-show="$store.sidebar.isOpen"
x-transition:enter="lg:transition lg:delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
@endif
>
<x-filament::badge
:color="$badgeColor"
:tooltip="$badgeTooltip"
>
{{ $badge }}
</x-filament::badge>
</span>
@endif
</a>
@if (($active || $activeChildItems) && $childItems)
<ul class="fi-sidebar-sub-group-items flex flex-col gap-y-1">
@foreach ($childItems as $childItem)
<x-filament-panels::sidebar.item
:active="$childItem->isActive()"
:active-child-items="$childItem->isChildItemsActive()"
:active-icon="$childItem->getActiveIcon()"
:badge="$childItem->getBadge()"
:badge-color="$childItem->getBadgeColor()"
:badge-tooltip="$childItem->getBadgeTooltip()"
:first="$loop->first"
grouped
:icon="$childItem->getIcon()"
:last="$loop->last"
:should-open-url-in-new-tab="$childItem->shouldOpenUrlInNewTab()"
sub-grouped
:url="$childItem->getUrl()"
>
{{ $childItem->getLabel() }}
</x-filament-panels::sidebar.item>
@endforeach
</ul>
@endif
</li>

View file

@ -0,0 +1,168 @@
@php
$currentTenant = filament()->getTenant();
$currentTenantName = filament()->getTenantName($currentTenant);
$items = filament()->getTenantMenuItems();
$billingItem = $items['billing'] ?? null;
$billingItemUrl = $billingItem?->getUrl();
$isBillingItemVisible = $billingItem?->isVisible() ?? true;
$hasBillingItem = (filament()->hasTenantBilling() || filled($billingItemUrl)) && $isBillingItemVisible;
$registrationItem = $items['register'] ?? null;
$registrationItemUrl = $registrationItem?->getUrl();
$isRegistrationItemVisible = $registrationItem?->isVisible() ?? true;
$hasRegistrationItem = ((filament()->hasTenantRegistration() && filament()->getTenantRegistrationPage()::canView()) || filled($registrationItemUrl)) && $isRegistrationItemVisible;
$profileItem = $items['profile'] ?? null;
$profileItemUrl = $profileItem?->getUrl();
$isProfileItemVisible = $profileItem?->isVisible() ?? true;
$hasProfileItem = ((filament()->hasTenantProfile() && filament()->getTenantProfilePage()::canView($currentTenant)) || filled($profileItemUrl)) && $isProfileItemVisible;
$canSwitchTenants = count($tenants = array_filter(
filament()->getUserTenants(filament()->auth()->user()),
fn (\Illuminate\Database\Eloquent\Model $tenant): bool => ! $tenant->is($currentTenant),
));
$items = \Illuminate\Support\Arr::except($items, ['billing', 'profile', 'register']);
@endphp
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TENANT_MENU_BEFORE) }}
<x-filament::dropdown
placement="bottom-start"
size
teleport
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-tenant-menu'])
"
>
<x-slot name="trigger">
<button
@if (filament()->isSidebarCollapsibleOnDesktop())
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($currentTenantName),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
@endif
type="button"
class="fi-tenant-menu-trigger group flex w-full items-center justify-center gap-x-3 rounded p-2 text-sm font-medium outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<x-filament-panels::avatar.tenant
:tenant="$currentTenant"
class="shrink-0"
/>
<span
@if (filament()->isSidebarCollapsibleOnDesktop())
x-show="$store.sidebar.isOpen"
@endif
class="grid justify-items-start text-start"
>
@if ($currentTenant instanceof \Filament\Models\Contracts\HasCurrentTenantLabel)
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ $currentTenant->getCurrentTenantLabel() }}
</span>
@endif
<span class="text-gray-950 dark:text-white">
{{ $currentTenantName }}
</span>
</span>
<x-filament::icon
icon="heroicon-m-chevron-down"
icon-alias="panels::tenant-menu.toggle-button"
:x-show="filament()->isSidebarCollapsibleOnDesktop() ? '$store.sidebar.isOpen' : null"
class="ms-auto h-5 w-5 shrink-0 text-gray-400 transition duration-75 group-hover:text-gray-500 group-focus-visible:text-gray-500 dark:text-gray-500 dark:group-hover:text-gray-400 dark:group-focus-visible:text-gray-400"
/>
</button>
</x-slot>
@if ($hasProfileItem || $hasBillingItem)
<x-filament::dropdown.list>
@if ($hasProfileItem)
<x-filament::dropdown.list.item
:color="$profileItem?->getColor()"
:href="$profileItemUrl ?? filament()->getTenantProfileUrl()"
:icon="$profileItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::tenant-menu.profile-button') ?? 'heroicon-m-cog-6-tooth'"
tag="a"
:target="($profileItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
>
{{ $profileItem?->getLabel() ?? filament()->getTenantProfilePage()::getLabel() }}
</x-filament::dropdown.list.item>
@endif
@if ($hasBillingItem)
<x-filament::dropdown.list.item
:color="$billingItem?->getColor() ?? 'gray'"
:href="$billingItemUrl ?? filament()->getTenantBillingUrl()"
:icon="$billingItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::tenant-menu.billing-button') ?? 'heroicon-m-credit-card'"
tag="a"
:target="($billingItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
>
{{ $billingItem?->getLabel() ?? __('filament-panels::layout.actions.billing.label') }}
</x-filament::dropdown.list.item>
@endif
</x-filament::dropdown.list>
@endif
@if (count($items))
<x-filament::dropdown.list>
@foreach ($items as $item)
@php
$itemPostAction = $item->getPostAction();
@endphp
<x-filament::dropdown.list.item
:action="$itemPostAction"
:color="$item->getColor()"
:href="$item->getUrl()"
:icon="$item->getIcon()"
:method="filled($itemPostAction) ? 'post' : null"
:tag="filled($itemPostAction) ? 'form' : 'a'"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endif
@if ($canSwitchTenants)
<x-filament::dropdown.list>
@foreach ($tenants as $tenant)
<x-filament::dropdown.list.item
:href="filament()->getUrl($tenant)"
:image="filament()->getTenantAvatarUrl($tenant)"
tag="a"
>
{{ filament()->getTenantName($tenant) }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endif
@if ($hasRegistrationItem)
<x-filament::dropdown.list>
<x-filament::dropdown.list.item
:color="$registrationItem?->getColor()"
:href="$registrationItemUrl ?? filament()->getTenantRegistrationUrl()"
:icon="$registrationItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::tenant-menu.registration-button') ?? 'heroicon-m-plus'"
tag="a"
:target="($registrationItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
>
{{ $registrationItem?->getLabel() ?? filament()->getTenantRegistrationPage()::getLabel() }}
</x-filament::dropdown.list.item>
</x-filament::dropdown.list>
@endif
</x-filament::dropdown>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TENANT_MENU_AFTER) }}

View file

@ -0,0 +1,30 @@
@props([
'icon',
'theme',
])
@php
$label = __("filament-panels::layout.actions.theme_switcher.{$theme}.label");
@endphp
<button
aria-label="{{ $label }}"
type="button"
x-on:click="(theme = @js($theme)) && close()"
x-tooltip="{
content: @js($label),
theme: $store.theme,
}"
class="fi-theme-switcher-btn flex justify-center rounded-md p-2 outline-none transition duration-75 hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
x-bind:class="
theme === @js($theme)
? 'fi-active bg-gray-50 text-primary-500 dark:bg-white/5 dark:text-primary-400'
: 'text-gray-400 hover:text-gray-500 focus-visible:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 dark:focus-visible:text-gray-400'
"
>
<x-filament::icon
:alias="'panels::theme-switcher.' . $theme . '-button'"
:icon="$icon"
class="h-5 w-5"
/>
</button>

View file

@ -0,0 +1,26 @@
<div
x-data="{ theme: null }"
x-init="
$watch('theme', () => {
$dispatch('theme-changed', theme)
})
theme = localStorage.getItem('theme') || @js(filament()->getDefaultThemeMode()->value)
"
class="fi-theme-switcher grid grid-flow-col gap-x-1"
>
<x-filament-panels::theme-switcher.button
icon="heroicon-m-sun"
theme="light"
/>
<x-filament-panels::theme-switcher.button
icon="heroicon-m-moon"
theme="dark"
/>
<x-filament-panels::theme-switcher.button
icon="heroicon-m-computer-desktop"
theme="system"
/>
</div>

View file

@ -0,0 +1,9 @@
<x-filament::icon-button
:badge="$unreadNotificationsCount"
color="gray"
icon="heroicon-o-bell"
icon-alias="panels::topbar.open-database-notifications-button"
icon-size="lg"
:label="__('filament-panels::layout.actions.open_database_notifications.label')"
class="fi-topbar-database-notifications-btn"
/>

View file

@ -0,0 +1,177 @@
@props([
'navigation',
])
<div
{{
$attributes->class([
'fi-topbar sticky top-0 z-20 overflow-x-clip',
'fi-topbar-with-navigation' => filament()->hasTopNavigation(),
])
}}
>
<nav
class="flex h-16 items-center gap-x-4 bg-white px-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 md:px-6 lg:px-8"
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_START) }}
@if (filament()->hasNavigation())
<x-filament::icon-button
color="gray"
icon="heroicon-o-bars-3"
icon-alias="panels::topbar.open-sidebar-button"
icon-size="lg"
:label="__('filament-panels::layout.actions.sidebar.expand.label')"
x-cloak
x-data="{}"
x-on:click="$store.sidebar.open()"
x-show="! $store.sidebar.isOpen"
@class([
'fi-topbar-open-sidebar-btn',
'lg:hidden' => (! filament()->isSidebarFullyCollapsibleOnDesktop()) || filament()->isSidebarCollapsibleOnDesktop(),
])
/>
<x-filament::icon-button
color="gray"
icon="heroicon-o-x-mark"
icon-alias="panels::topbar.close-sidebar-button"
icon-size="lg"
:label="__('filament-panels::layout.actions.sidebar.collapse.label')"
x-cloak
x-data="{}"
x-on:click="$store.sidebar.close()"
x-show="$store.sidebar.isOpen"
class="fi-topbar-close-sidebar-btn lg:hidden"
/>
@endif
@if (filament()->hasTopNavigation() || (! filament()->hasNavigation()))
<div class="me-6 hidden lg:flex">
@if ($homeUrl = filament()->getHomeUrl())
<a {{ \Filament\Support\generate_href_html($homeUrl) }}>
<x-filament-panels::logo />
</a>
@else
<x-filament-panels::logo />
@endif
</div>
@if (filament()->hasTenancy() && filament()->hasTenantMenu())
<x-filament-panels::tenant-menu class="hidden lg:block" />
@endif
@if (filament()->hasNavigation())
<ul class="me-4 hidden items-center gap-x-4 lg:flex">
@foreach ($navigation as $group)
@if ($groupLabel = $group->getLabel())
<x-filament::dropdown
placement="bottom-start"
teleport
:attributes="\Filament\Support\prepare_inherited_attributes($group->getExtraTopbarAttributeBag())"
>
<x-slot name="trigger">
<x-filament-panels::topbar.item
:active="$group->isActive()"
:icon="$group->getIcon()"
>
{{ $groupLabel }}
</x-filament-panels::topbar.item>
</x-slot>
@php
$lists = [];
foreach ($group->getItems() as $item) {
if ($childItems = $item->getChildItems()) {
$lists[] = [
$item,
...$childItems,
];
$lists[] = [];
continue;
}
if (empty($lists)) {
$lists[] = [$item];
continue;
}
$lists[count($lists) - 1][] = $item;
}
if (empty($lists[count($lists) - 1])) {
array_pop($lists);
}
@endphp
@foreach ($lists as $list)
<x-filament::dropdown.list>
@foreach ($list as $item)
@php
$itemIsActive = $item->isActive();
@endphp
<x-filament::dropdown.list.item
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:color="$itemIsActive ? 'primary' : 'gray'"
:href="$item->getUrl()"
:icon="$itemIsActive ? ($item->getActiveIcon() ?? $item->getIcon()) : $item->getIcon()"
tag="a"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
@endforeach
</x-filament::dropdown>
@else
@foreach ($group->getItems() as $item)
<x-filament-panels::topbar.item
:active="$item->isActive()"
:active-icon="$item->getActiveIcon()"
:badge="$item->getBadge()"
:badge-color="$item->getBadgeColor()"
:badge-tooltip="$item->getBadgeTooltip()"
:icon="$item->getIcon()"
:should-open-url-in-new-tab="$item->shouldOpenUrlInNewTab()"
:url="$item->getUrl()"
>
{{ $item->getLabel() }}
</x-filament-panels::topbar.item>
@endforeach
@endif
@endforeach
</ul>
@endif
@endif
<div
x-persist="topbar.end"
class="ms-auto flex items-center gap-x-4"
>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::GLOBAL_SEARCH_BEFORE) }}
@if (filament()->isGlobalSearchEnabled())
@livewire(Filament\Livewire\GlobalSearch::class, ['lazy' => true])
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::GLOBAL_SEARCH_AFTER) }}
@if (filament()->auth()->check())
@if (filament()->hasDatabaseNotifications())
@livewire(Filament\Livewire\DatabaseNotifications::class, ['lazy' => true])
@endif
<x-filament-panels::user-menu />
@endif
</div>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::TOPBAR_END) }}
</nav>
</div>

View file

@ -0,0 +1,77 @@
@props([
'active' => false,
'activeIcon' => null,
'badge' => null,
'badgeColor' => null,
'badgeTooltip' => null,
'icon' => null,
'shouldOpenUrlInNewTab' => false,
'url' => null,
])
@php
$tag = $url ? 'a' : 'button';
@endphp
<li
@class([
'fi-topbar-item',
// @deprecated `fi-topbar-item-active` has been replaced by `fi-active`.
'fi-active fi-topbar-item-active' => $active,
])
>
<{{ $tag }}
@if ($url)
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
@else
type="button"
@endif
@class([
'fi-topbar-item-button flex items-center justify-center gap-x-2 rounded px-3 py-2 outline-none transition duration-75 hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5',
'bg-gray-50 dark:bg-white/5' => $active,
])
>
@if ($icon || $activeIcon)
<x-filament::icon
:icon="($active && $activeIcon) ? $activeIcon : $icon"
@class([
'fi-topbar-item-icon h-5 w-5',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
@endif
<span
@class([
'fi-topbar-item-label text-sm font-medium',
'text-gray-700 dark:text-gray-200' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
>
{{ $slot }}
</span>
@if (filled($badge))
<x-filament::badge
:color="$badgeColor"
size="sm"
:tooltip="$badgeTooltip"
>
{{ $badge }}
</x-filament::badge>
@endif
@if (! $url)
<x-filament::icon
icon="heroicon-m-chevron-down"
icon-alias="panels::topbar.group.toggle-button"
@class([
'fi-topbar-group-toggle-icon h-5 w-5',
'text-gray-400 dark:text-gray-500' => ! $active,
'text-primary-600 dark:text-primary-400' => $active,
])
/>
@endif
</{{ $tag }}>
</li>

View file

@ -0,0 +1,31 @@
@if (filament()->hasUnsavedChangesAlerts())
@script
<script>
window.addEventListener('beforeunload', (event) => {
if (
[
...(@js($this instanceof \Filament\Actions\Contracts\HasActions) ? $wire.mountedActions ?? [] : []),
...(@js($this instanceof \Filament\Forms\Contracts\HasForms)
? $wire.mountedFormComponentActions ?? []
: []),
...(@js($this instanceof \Filament\Infolists\Contracts\HasInfolists) ? $wire.mountedInfolistActions ?? [] : []),
...(@js($this instanceof \Filament\Tables\Contracts\HasTable)
? [
...($wire.mountedTableActions ?? []),
...($wire.mountedTableBulkAction
? [$wire.mountedTableBulkAction]
: []),
]
: []),
].length &&
!$wire?.__instance?.effects?.redirect
) {
event.preventDefault()
event.returnValue = true
return
}
})
</script>
@endscript
@endif

View file

@ -0,0 +1,99 @@
@php
$user = filament()->auth()->user();
$items = filament()->getUserMenuItems();
$profileItem = $items['profile'] ?? $items['account'] ?? null;
$profileItemUrl = $profileItem?->getUrl();
$profilePage = filament()->getProfilePage();
$hasProfileItem = filament()->hasProfile() || filled($profileItemUrl);
$logoutItem = $items['logout'] ?? null;
$items = \Illuminate\Support\Arr::except($items, ['account', 'logout', 'profile']);
@endphp
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::USER_MENU_BEFORE) }}
<x-filament::dropdown
placement="bottom-end"
teleport
:attributes="
\Filament\Support\prepare_inherited_attributes($attributes)
->class(['fi-user-menu'])
"
>
<x-slot name="trigger">
<button
aria-label="{{ __('filament-panels::layout.actions.open_user_menu.label') }}"
type="button"
class="shrink-0"
>
<x-filament-panels::avatar.user :user="$user" />
</button>
</x-slot>
@if ($profileItem?->isVisible() ?? true)
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::USER_MENU_PROFILE_BEFORE) }}
@if ($hasProfileItem)
<x-filament::dropdown.list>
<x-filament::dropdown.list.item
:color="$profileItem?->getColor()"
:icon="$profileItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::user-menu.profile-item') ?? 'heroicon-m-user-circle'"
:href="$profileItemUrl ?? filament()->getProfileUrl()"
:target="($profileItem?->shouldOpenUrlInNewTab() ?? false) ? '_blank' : null"
tag="a"
>
{{ $profileItem?->getLabel() ?? ($profilePage ? $profilePage::getLabel() : null) ?? filament()->getUserName($user) }}
</x-filament::dropdown.list.item>
</x-filament::dropdown.list>
@else
<x-filament::dropdown.header
:color="$profileItem?->getColor()"
:icon="$profileItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::user-menu.profile-item') ?? 'heroicon-m-user-circle'"
>
{{ $profileItem?->getLabel() ?? filament()->getUserName($user) }}
</x-filament::dropdown.header>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::USER_MENU_PROFILE_AFTER) }}
@endif
@if (filament()->hasDarkMode() && (! filament()->hasDarkModeForced()))
<x-filament::dropdown.list>
<x-filament-panels::theme-switcher />
</x-filament::dropdown.list>
@endif
<x-filament::dropdown.list>
@foreach ($items as $key => $item)
@php
$itemPostAction = $item->getPostAction();
@endphp
<x-filament::dropdown.list.item
:action="$itemPostAction"
:color="$item->getColor()"
:href="$item->getUrl()"
:icon="$item->getIcon()"
:method="filled($itemPostAction) ? 'post' : null"
:tag="filled($itemPostAction) ? 'form' : 'a'"
:target="$item->shouldOpenUrlInNewTab() ? '_blank' : null"
>
{{ $item->getLabel() }}
</x-filament::dropdown.list.item>
@endforeach
<x-filament::dropdown.list.item
:action="$logoutItem?->getUrl() ?? filament()->getLogoutUrl()"
:color="$logoutItem?->getColor()"
:icon="$logoutItem?->getIcon() ?? \Filament\Support\Facades\FilamentIcon::resolve('panels::user-menu.logout-button') ?? 'heroicon-m-arrow-left-on-rectangle'"
method="post"
tag="form"
>
{{ $logoutItem?->getLabel() ?? __('filament-panels::layout.actions.logout.label') }}
</x-filament::dropdown.list.item>
</x-filament::dropdown.list>
</x-filament::dropdown>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::USER_MENU_AFTER) }}

View file

@ -0,0 +1,12 @@
<x-dynamic-component
:component="static::isSimple() ? 'filament-panels::page.simple' : 'filament-panels::page'"
>
<x-filament-panels::form wire:submit="save">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
</x-dynamic-component>

View file

@ -0,0 +1,15 @@
<x-filament-panels::page.simple>
<p class="text-center text-sm text-gray-500 dark:text-gray-400">
{{
__('filament-panels::pages/auth/email-verification/email-verification-prompt.messages.notification_sent', [
'email' => filament()->auth()->user()->getEmailForVerification(),
])
}}
</p>
<p class="text-center text-sm text-gray-500 dark:text-gray-400">
{{ __('filament-panels::pages/auth/email-verification/email-verification-prompt.messages.notification_not_received') }}
{{ $this->resendNotificationAction }}
</p>
</x-filament-panels::page.simple>

View file

@ -0,0 +1,22 @@
<x-filament-panels::page.simple>
@if (filament()->hasRegistration())
<x-slot name="subheading">
{{ __('filament-panels::pages/auth/login.actions.register.before') }}
{{ $this->registerAction }}
</x-slot>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::form wire:submit="authenticate">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_LOGIN_FORM_AFTER, scopes: $this->getRenderHookScopes()) }}
</x-filament-panels::page.simple>

View file

@ -0,0 +1,20 @@
<x-filament-panels::page.simple>
@if (filament()->hasLogin())
<x-slot name="subheading">
{{ $this->loginAction }}
</x-slot>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_PASSWORD_RESET_REQUEST_FORM_BEFORE, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::form wire:submit="request">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_PASSWORD_RESET_REQUEST_FORM_AFTER, scopes: $this->getRenderHookScopes()) }}
</x-filament-panels::page.simple>

View file

@ -0,0 +1,14 @@
<x-filament-panels::page.simple>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_PASSWORD_RESET_RESET_FORM_BEFORE, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::form wire:submit="resetPassword">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_PASSWORD_RESET_RESET_FORM_AFTER, scopes: $this->getRenderHookScopes()) }}
</x-filament-panels::page.simple>

View file

@ -0,0 +1,22 @@
<x-filament-panels::page.simple>
@if (filament()->hasLogin())
<x-slot name="subheading">
{{ __('filament-panels::pages/auth/register.actions.login.before') }}
{{ $this->loginAction }}
</x-slot>
@endif
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_REGISTER_FORM_BEFORE, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::form wire:submit="register">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::AUTH_REGISTER_FORM_AFTER, scopes: $this->getRenderHookScopes()) }}
</x-filament-panels::page.simple>

View file

@ -0,0 +1,16 @@
<x-filament-panels::page class="fi-dashboard-page">
@if (method_exists($this, 'filtersForm'))
{{ $this->filtersForm }}
@endif
<x-filament-widgets::widgets
:columns="$this->getColumns()"
:data="
[
...(property_exists($this, 'filters') ? ['filters' => $this->filters] : []),
...$this->getWidgetData(),
]
"
:widgets="$this->getVisibleWidgets()"
/>
</x-filament-panels::page>

View file

@ -0,0 +1,10 @@
<x-filament-panels::page>
<x-filament-panels::form wire:submit="save">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
</x-filament-panels::page>

View file

@ -0,0 +1,10 @@
<x-filament-panels::page.simple>
<x-filament-panels::form wire:submit="register">
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
</x-filament-panels::page.simple>

View file

@ -0,0 +1,21 @@
<x-filament-panels::page
@class([
'fi-resource-create-record-page',
'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
])
>
<x-filament-panels::form
id="form"
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
wire:submit="create"
>
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page>

View file

@ -0,0 +1,50 @@
<x-filament-panels::page
@class([
'fi-resource-edit-record-page',
'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
'fi-resource-record-' . $record->getKey(),
])
>
@capture($form)
<x-filament-panels::form
id="form"
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
wire:submit="save"
>
{{ $this->form }}
<x-filament-panels::form.actions
:actions="$this->getCachedFormActions()"
:full-width="$this->hasFullWidthFormActions()"
/>
</x-filament-panels::form>
@endcapture
@php
$relationManagers = $this->getRelationManagers();
$hasCombinedRelationManagerTabsWithContent = $this->hasCombinedRelationManagerTabsWithContent();
@endphp
@if ((! $hasCombinedRelationManagerTabsWithContent) || (! count($relationManagers)))
{{ $form() }}
@endif
@if (count($relationManagers))
<x-filament-panels::resources.relation-managers
:active-locale="isset($activeLocale) ? $activeLocale : null"
:active-manager="$this->activeRelationManager ?? ($hasCombinedRelationManagerTabsWithContent ? null : array_key_first($relationManagers))"
:content-tab-label="$this->getContentTabLabel()"
:managers="$relationManagers"
:owner-record="$record"
:page-class="static::class"
>
@if ($hasCombinedRelationManagerTabsWithContent)
<x-slot name="content">
{{ $form() }}
</x-slot>
@endif
</x-filament-panels::resources.relation-managers>
@endif
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page>

View file

@ -0,0 +1,16 @@
<x-filament-panels::page
@class([
'fi-resource-list-records-page',
'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
])
>
<div class="flex flex-col gap-y-6">
<x-filament-panels::resources.tabs />
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_BEFORE, scopes: $this->getRenderHookScopes()) }}
{{ $this->table }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_LIST_RECORDS_TABLE_AFTER, scopes: $this->getRenderHookScopes()) }}
</div>
</x-filament-panels::page>

View file

@ -0,0 +1,28 @@
<x-filament-panels::page
@class([
'fi-resource-manage-related-records-page',
'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
])
>
@if ($this->table->getColumns())
<div class="flex flex-col gap-y-6">
<x-filament-panels::resources.tabs />
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_MANAGE_RELATED_RECORDS_TABLE_BEFORE, scopes: $this->getRenderHookScopes()) }}
{{ $this->table }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_PAGES_MANAGE_RELATED_RECORDS_TABLE_AFTER, scopes: $this->getRenderHookScopes()) }}
</div>
@endif
@if (count($relationManagers = $this->getRelationManagers()))
<x-filament-panels::resources.relation-managers
:active-locale="isset($activeLocale) ? $activeLocale : null"
:active-manager="$this->activeRelationManager ?? array_key_first($relationManagers)"
:managers="$relationManagers"
:owner-record="$record"
:page-class="static::class"
/>
@endif
</x-filament-panels::page>

View file

@ -0,0 +1,45 @@
<x-filament-panels::page
@class([
'fi-resource-view-record-page',
'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
'fi-resource-record-' . $record->getKey(),
])
>
@php
$relationManagers = $this->getRelationManagers();
$hasCombinedRelationManagerTabsWithContent = $this->hasCombinedRelationManagerTabsWithContent();
@endphp
@if ((! $hasCombinedRelationManagerTabsWithContent) || (! count($relationManagers)))
@if ($this->hasInfolist())
{{ $this->infolist }}
@else
<div
wire:key="{{ $this->getId() }}.forms.{{ $this->getFormStatePath() }}"
>
{{ $this->form }}
</div>
@endif
@endif
@if (count($relationManagers))
<x-filament-panels::resources.relation-managers
:active-locale="isset($activeLocale) ? $activeLocale : null"
:active-manager="$this->activeRelationManager ?? ($hasCombinedRelationManagerTabsWithContent ? null : array_key_first($relationManagers))"
:content-tab-label="$this->getContentTabLabel()"
:managers="$relationManagers"
:owner-record="$record"
:page-class="static::class"
>
@if ($hasCombinedRelationManagerTabsWithContent)
<x-slot name="content">
@if ($this->hasInfolist())
{{ $this->infolist }}
@else
{{ $this->form }}
@endif
</x-slot>
@endif
</x-filament-panels::resources.relation-managers>
@endif
</x-filament-panels::page>

View file

@ -0,0 +1,11 @@
<div class="fi-resource-relation-manager flex flex-col gap-y-6">
<x-filament-panels::resources.tabs />
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_RELATION_MANAGER_BEFORE, scopes: $this->getRenderHookScopes()) }}
{{ $this->table }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\View\PanelsRenderHook::RESOURCE_RELATION_MANAGER_AFTER, scopes: $this->getRenderHookScopes()) }}
<x-filament-panels::unsaved-action-changes-alert />
</div>

View file

@ -0,0 +1,42 @@
@php
$user = filament()->auth()->user();
@endphp
<x-filament-widgets::widget class="fi-account-widget">
<x-filament::section>
<div class="flex items-center gap-x-3">
<x-filament-panels::avatar.user size="lg" :user="$user" />
<div class="flex-1">
<h2
class="grid flex-1 text-base font-semibold leading-6 text-gray-950 dark:text-white"
>
{{ __('filament-panels::widgets/account-widget.welcome', ['app' => config('app.name')]) }}
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ filament()->getUserName($user) }}
</p>
</div>
<form
action="{{ filament()->getLogoutUrl() }}"
method="post"
class="my-auto"
>
@csrf
<x-filament::button
color="gray"
icon="heroicon-m-arrow-left-on-rectangle"
icon-alias="panels::widgets.account.logout-button"
labeled-from="sm"
tag="button"
type="submit"
>
{{ __('filament-panels::widgets/account-widget.actions.logout.label') }}
</x-filament::button>
</form>
</div>
</x-filament::section>
</x-filament-widgets::widget>

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ export default {
content: [ content: [
'./app/Filament/**/*.php', './app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php', './resources/views/filament/**/*.blade.php',
'./resources/views/vendor/**/*.blade.php',
'./vendor/filament/**/*.blade.php', './vendor/filament/**/*.blade.php',
], ],
}; };