filter dan stats

This commit is contained in:
hildaaaevs 2025-05-26 21:39:59 +07:00
parent 0ee947f611
commit 33f1cd84c1
44 changed files with 3288 additions and 70 deletions

View File

@ -33,6 +33,13 @@ class PaketFotoResource extends Resource
protected static ?string $model = PaketFoto::class; protected static ?string $model = PaketFoto::class;
protected static ?string $navigationIcon = 'heroicon-o-camera'; protected static ?string $navigationIcon = 'heroicon-o-camera';
protected static ?string $navigationLabel = 'Paket Foto';
public static function getPluralLabel(): string{
return 'Paket Foto';}
public static function getModelLabel(): string
{
return 'Paket Foto';
}
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
@ -87,9 +94,9 @@ public static function table(Table $table): Table
]) ])
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ // Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), // Tables\Actions\DeleteBulkAction::make(),
]), //]),
]); ]);
} }

View File

@ -27,6 +27,13 @@ class PromoResource extends Resource
protected static ?string $model = Promo::class; protected static ?string $model = Promo::class;
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar'; protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static ?string $navigationLabel = 'Promo';
public static function getPluralLabel(): string{
return 'Promo';}
public static function getModelLabel(): string
{
return 'Promo';
}
public static function form(Form $form): Form public static function form(Form $form): Form
{ {

View File

@ -38,6 +38,13 @@ class ReservasiiResource extends Resource
protected static ?string $model = Reservasii::class; protected static ?string $model = Reservasii::class;
protected static ?string $navigationIcon = 'heroicon-o-squares-plus'; protected static ?string $navigationIcon = 'heroicon-o-squares-plus';
protected static ?string $navigationLabel = 'Reservasi';
public static function getPluralLabel(): string{
return 'Reservasi';}
public static function getModelLabel(): string
{
return 'Reservasi';
}
public static function form(Form $form): Form public static function form(Form $form): Form
{ {
@ -214,36 +221,17 @@ public static function table(Table $table): Table
Tables\Columns\TextColumn::make('metode_pembayaran') Tables\Columns\TextColumn::make('metode_pembayaran')
->badge(), ->badge(),
Tables\Columns\TextColumn::make('created_at')
->label('Tanggal Dibuat')
->dateTime('d F Y - H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
]) ])
->filters([ ->filters([
Tables\Filters\SelectFilter::make('tipe_pembayaran')
->options([
'full' => 'Full Payment',
'dp' => 'Down Payment',
])
->label('Status Pembayaran'),
Tables\Filters\Filter::make('tanggal') Tables\Filters\Filter::make('tanggal')
->form([ ->form([
Forms\Components\DatePicker::make('tanggal_from')->label('Dari Tanggal'), DatePicker::make('tanggal')->label('Tanggal'),
Forms\Components\DatePicker::make('tanggal_until')->label('Sampai Tanggal'),
]) ])
->query(function (Builder $query, array $data): Builder { ->query(function (Builder $query, array $data): Builder {
return $query return $query->when(
->when( $data['tanggal'],
$data['tanggal_from'], fn (Builder $query, $date): Builder => $query->whereDate('tanggal', $date),
fn (Builder $query, $date): Builder => $query->whereDate('tanggal', '>=', $date), );
)
->when(
$data['tanggal_until'],
fn (Builder $query, $date): Builder => $query->whereDate('tanggal', '<=', $date),
);
}), }),
]) ])
->actions([ ->actions([

View File

@ -5,6 +5,8 @@
use App\Filament\Resources\ReservasiiResource; use App\Filament\Resources\ReservasiiResource;
use App\Filament\Resources\ReservasiiResource\Widgets\ReservasiiStats; use App\Filament\Resources\ReservasiiResource\Widgets\ReservasiiStats;
use Filament\Actions; use Filament\Actions;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Resources\Components\Tabs;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListReservasiis extends ListRecords class ListReservasiis extends ListRecords
@ -18,9 +20,19 @@ protected function getHeaderActions(): array
]; ];
} }
protected function getHeaderWidgets(): array{ //protected function getHeaderWidgets(): array{
// return[
// ReservasiiStats::class
//];
//}
public function getTabs(): array{
return[ return[
ReservasiiStats::class null => \Filament\Resources\Components\Tab::make('Semua'),
'Paket Pasangan' => \Filament\Resources\Components\Tab::make()->query(fn($query) => $query->whereRelation('detail.paketFoto', 'nama_paket_foto', 'Paket Pasangan')),
'Paket 5 orang' => \Filament\Resources\Components\Tab::make()->query(fn($query) => $query->whereRelation('detail.paketFoto', 'nama_paket_foto', 'Paket 5 Orang')),
'Widebox Couple' => \Filament\Resources\Components\Tab::make()->query(fn($query) => $query->whereRelation('detail.paketFoto', 'nama_paket_foto', 'Widebox Couple')),
'Widebox Group' => \Filament\Resources\Components\Tab::make()->query(fn($query) => $query->whereRelation('detail.paketFoto', 'nama_paket_foto', 'Widebox Group')),
]; ];
} }
} }

View File

@ -13,10 +13,15 @@ protected function getStats(): array{
$todayEarnings = Reservasii::query() $todayEarnings = Reservasii::query()
->whereDate('tanggal', Carbon::today()) ->whereDate('tanggal', Carbon::today())
->sum('total'); ->sum('total');
$mounthEarnings = Reservasii::whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->sum('total');
return [ return [
Stat::make('Reservasi Hari Ini', Reservasii::query()->whereDate('tanggal', Carbon::today())->count()), Stat::make('Reservasi Hari Ini', Reservasii::query()->whereDate('tanggal', Carbon::today())->count()),
Stat::make('Pendapatan Hari Ini', 'Rp ' . number_format($todayEarnings, 0, ',', '.')) Stat::make('Pendapatan Hari Ini', 'Rp' . number_format($todayEarnings, 0, ',', '.')),
Stat::make('Pendapatan Per Bulan','Rp' . number_format($mounthEarnings, 0,',', '.'))
]; ];
} }
} }

View File

@ -26,7 +26,13 @@ class UserResource extends Resource
{ {
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
protected static ?string $navigationLabel = 'User';
public static function getPluralLabel(): string{
return 'User';}
public static function getModelLabel(): string
{
return 'User';
}
protected static ?string $navigationIcon = 'heroicon-o-user-group'; protected static ?string $navigationIcon = 'heroicon-o-user-group';
public static function form(Form $form): Form public static function form(Form $form): Form
@ -78,9 +84,9 @@ public static function table(Table $table): Table
]) ])
]) ])
->bulkActions([ ->bulkActions([
Tables\Actions\BulkActionGroup::make([ // Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(), // Tables\Actions\DeleteBulkAction::make(),
]), //]),
]); ]);
} }

View File

@ -1,33 +0,0 @@
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\Widget;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class CalendarWidget extends Widget
{
protected static string $view = 'filament.widgets.calendar-widget';
protected static ?string $heading = 'Jadwal Reservasi';
protected static ?int $sort = 1;
protected int | string | array $columnSpan = 'full';
public function getReservations()
{
$data = DB::table('reservasiis')
->select('tanggal', 'waktu')
->get()
->groupBy(function($item) {
return Carbon::parse($item->tanggal)->format('Y-m-d');
})
->map(function ($items) {
return $items->pluck('waktu')->toArray();
});
return $data;
}
}

View File

@ -31,8 +31,11 @@ public function panel(Panel $panel): Panel
->path('admin') ->path('admin')
->login() ->login()
->colors([ ->colors([
'primary' => Color::Amber, 'primary' => Color::Gray,
]) ])
->brandName('Ko-La Self')
->brandLogo(asset('images/logo.png'))
->favicon(asset('images/logo.png'))
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([ ->pages([
@ -41,7 +44,7 @@ public function panel(Panel $panel): Panel
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
ReservasiiStats::class, ReservasiiStats::class,
CalendarWidget::class //CalendarWidget::class
//Widgets\AccountWidget::class, //Widgets\AccountWidget::class,
//Widgets\FilamentInfoWidget::class, //Widgets\FilamentInfoWidget::class,
]) ])

View File

@ -0,0 +1,17 @@
@if (isset($data))
<script>
window.filamentData = @js($data)
</script>
@endif
@foreach ($assets as $asset)
@if (! $asset->isLoadedOnRequest())
{{ $asset->getHtml() }}
@endif
@endforeach
<style>
:root {
@foreach ($cssVariables ?? [] as $cssVariableName => $cssVariableValue) --{{ $cssVariableName }}:{{ $cssVariableValue }}; @endforeach
}
</style>

View File

@ -0,0 +1,64 @@
@php
use Filament\Support\Enums\Alignment;
@endphp
@props([
'actions' => [],
'alignment' => Alignment::Start,
'fullWidth' => false,
])
@php
if (is_array($actions)) {
$actions = array_filter(
$actions,
fn ($action): bool => $action->isVisible(),
);
}
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
$hasActions = false;
$hasSlot = ! \Filament\Support\is_slot_empty($slot);
$actionsAreHtmlable = $actions instanceof \Illuminate\Contracts\Support\Htmlable;
if ($hasSlot) {
$hasActions = true;
} elseif ($actionsAreHtmlable) {
$hasActions = ! \Filament\Support\is_slot_empty($actions);
} else {
$hasActions = filled($actions);
}
@endphp
@if ($hasActions)
<div
{{
$attributes->class([
'fi-ac gap-3',
'flex flex-wrap items-center' => ! $fullWidth,
match ($alignment) {
Alignment::Start, Alignment::Left => 'justify-start',
Alignment::Center => 'justify-center',
Alignment::End, Alignment::Right => 'flex-row-reverse',
Alignment::Between, Alignment::Justify => 'justify-between',
default => $alignment,
} => ! $fullWidth,
'grid grid-cols-[repeat(auto-fit,minmax(0,1fr))]' => $fullWidth,
])
}}
>
@if ($hasSlot)
{{ $slot }}
@elseif ($actionsAreHtmlable)
{{ $actions }}
@else
@foreach ($actions as $action)
{{ $action }}
@endforeach
@endif
</div>
@endif

View File

@ -0,0 +1,21 @@
@props([
'circular' => true,
'size' => 'md',
])
<img
{{
$attributes
->class([
'fi-avatar object-cover object-center',
'rounded-md' => ! $circular,
'fi-circular rounded-full' => $circular,
match ($size) {
'sm' => 'h-6 w-6',
'md' => 'h-8 w-8',
'lg' => 'h-10 w-10',
default => $size,
},
])
}}
/>

View File

@ -0,0 +1,238 @@
@php
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
@endphp
@props([
'color' => 'primary',
'deleteButton' => null,
'disabled' => false,
'form' => null,
'formId' => null,
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconPosition' => IconPosition::Before,
'iconSize' => IconSize::Small,
'keyBindings' => null,
'loadingIndicator' => true,
'size' => ActionSize::Medium,
'spaMode' => null,
'tag' => 'span',
'target' => null,
'tooltip' => null,
'type' => 'button',
])
@php
if (! $iconPosition instanceof IconPosition) {
$iconPosition = filled($iconPosition) ? (IconPosition::tryFrom($iconPosition) ?? $iconPosition) : null;
}
if (! $size instanceof ActionSize) {
$size = filled($size) ? (ActionSize::tryFrom($size) ?? $size) : null;
}
if (! $iconSize instanceof IconSize) {
$iconSize = filled($iconSize) ? (IconSize::tryFrom($iconSize) ?? $iconSize) : null;
}
$isDeletable = count($deleteButton?->attributes->getAttributes() ?? []) > 0;
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-badge-icon h-4 w-4',
match ($iconSize) {
IconSize::Small => 'h-4 w-4',
IconSize::Medium => 'h-5 w-5',
IconSize::Large => 'h-6 w-6',
default => $iconSize,
},
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500',
},
]);
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [500],
alias: 'badge.icon',
) => $color !== 'gray',
]);
$wireTarget = $loadingIndicator ? $attributes->whereStartsWith(['wire:target', 'wire:click'])->filter(fn ($value): bool => filled($value))->first() : null;
$hasLoadingIndicator = filled($wireTarget) || ($type === 'submit' && filled($form));
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget ?: $form, ENT_QUOTES);
}
$hasTooltip = filled($tooltip);
@endphp
<{{ $tag }}
@if ($tag === 'a')
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@endif
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->merge([
'disabled' => $disabled,
'form' => $tag === 'button' ? $formId : null,
'type' => $tag === 'button' ? $type : null,
'wire:loading.attr' => $tag === 'button' ? 'disabled' : null,
'wire:target' => ($hasLoadingIndicator && $loadingIndicatorTarget) ? $loadingIndicatorTarget : null,
], escape: false)
->class([
'fi-badge flex items-center justify-center gap-x-1 rounded-md text-xs font-medium ring-1 ring-inset',
'pointer-events-none opacity-70' => $disabled,
match ($size) {
ActionSize::ExtraSmall => 'px-0.5 min-w-[theme(spacing.4)] tracking-tighter',
ActionSize::Small => 'px-1.5 min-w-[theme(spacing.5)] py-0.5 tracking-tight',
ActionSize::Medium, ActionSize::Large, ActionSize::ExtraLarge => 'px-2 min-w-[theme(spacing.6)] py-1',
default => $size,
},
match ($color) {
'gray' => 'bg-gray-50 text-gray-600 ring-gray-600/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20',
default => 'fi-color-custom bg-custom-50 text-custom-600 ring-custom-600/10 dark:bg-custom-400/10 dark:text-custom-400 dark:ring-custom-400/30',
},
is_string($color) ? "fi-color-{$color}" : null,
])
->style([
\Filament\Support\get_color_css_variables(
$color,
shades: [
50,
400,
600,
],
alias: 'badge',
) => $color !== 'gray',
])
}}
>
@if ($iconPosition === IconPosition::Before)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@endif
<span class="grid">
<span class="truncate">
{{ $slot }}
</span>
</span>
@if ($isDeletable)
<button
type="button"
{{
$deleteButton
->attributes
->except(['label'])
->class([
'fi-badge-delete-button -my-1 -me-2 -ms-1 flex items-center justify-center p-1 outline-none transition duration-75',
match ($color) {
'gray' => 'text-gray-700/50 hover:text-gray-700/75 focus-visible:text-gray-700/75 dark:text-gray-300/50 dark:hover:text-gray-300/75 dark:focus-visible:text-gray-300/75',
default => 'text-custom-700/50 hover:text-custom-700/75 focus-visible:text-custom-700/75 dark:text-custom-300/50 dark:hover:text-custom-300/75 dark:focus-visible:text-custom-300/75',
},
])
->style([
\Filament\Support\get_color_css_variables(
$color,
shades: [300, 700],
alias: 'badge.delete-button',
) => $color !== 'gray',
])
}}
>
<x-filament::icon
alias="badge.delete-button"
icon="heroicon-m-x-mark"
class="h-3.5 w-3.5"
/>
@if (filled($label = $deleteButton->attributes->get('label')))
<span class="sr-only">
{{ $label }}
</span>
@endif
</button>
@elseif ($iconPosition === IconPosition::After)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@endif
</{{ $tag }}>

View File

@ -0,0 +1,50 @@
@props([
'breadcrumbs' => [],
])
@php
$iconClasses = 'fi-breadcrumbs-item-separator flex h-5 w-5 text-gray-400 dark:text-gray-500';
$itemLabelClasses = 'fi-breadcrumbs-item-label text-sm font-medium text-gray-500 dark:text-gray-400';
@endphp
<nav {{ $attributes->class(['fi-breadcrumbs']) }}>
<ol class="fi-breadcrumbs-list flex flex-wrap items-center gap-x-2">
@foreach ($breadcrumbs as $url => $label)
<li class="fi-breadcrumbs-item flex items-center gap-x-2">
@if (! $loop->first)
<x-filament::icon
alias="breadcrumbs.separator"
icon="heroicon-m-chevron-right"
@class([
$iconClasses,
'rtl:hidden',
])
/>
<x-filament::icon
{{-- @deprecated Use `breadcrubs.separator.rtl` instead of `breadcrumbs.separator` for RTL. --}}
:alias="['breadcrumbs.separator.rtl', 'breadcrumbs.separator']"
icon="heroicon-m-chevron-left"
@class([
$iconClasses,
'ltr:hidden',
])
/>
@endif
@if (is_int($url))
<span class="{{ $itemLabelClasses }}">
{{ $label }}
</span>
@else
<a
{{ \Filament\Support\generate_href_html($url) }}
class="{{ $itemLabelClasses }} transition duration-75 hover:text-gray-700 dark:hover:text-gray-200"
>
{{ $label }}
</a>
@endif
</li>
@endforeach
</ol>
</nav>

View File

@ -0,0 +1,9 @@
<div
{{
$attributes->class([
'fi-btn-group grid grid-flow-col rounded-lg shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20',
])
}}
>
{{ $slot }}
</div>

View File

@ -0,0 +1,332 @@
@php
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
@endphp
@props([
'badge' => null,
'badgeColor' => 'primary',
'badgeSize' => 'xs',
'color' => 'primary',
'disabled' => false,
'form' => null,
'formId' => null,
'grouped' => false,
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconPosition' => IconPosition::Before,
'iconSize' => null,
'keyBindings' => null,
'labeledFrom' => null,
'labelSrOnly' => false,
'loadingIndicator' => true,
'outlined' => false,
'size' => ActionSize::Medium,
'spaMode' => null,
'tag' => 'button',
'target' => null,
'tooltip' => null,
'type' => 'button',
])
@php
if (! $iconPosition instanceof IconPosition) {
$iconPosition = filled($iconPosition) ? (IconPosition::tryFrom($iconPosition) ?? $iconPosition) : null;
}
if (! $size instanceof ActionSize) {
$size = filled($size) ? (ActionSize::tryFrom($size) ?? $size) : null;
}
$iconSize ??= match ($size) {
ActionSize::ExtraSmall, ActionSize::Small => IconSize::Small,
default => IconSize::Medium,
};
if (! $iconSize instanceof IconSize) {
$iconSize = filled($iconSize) ? (IconSize::tryFrom($iconSize) ?? $iconSize) : null;
}
$buttonClasses = \Illuminate\Support\Arr::toCssClasses([
...[
'fi-btn relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus-visible:ring-2',
'pointer-events-none opacity-70' => $disabled,
'rounded-lg' => ! $grouped,
'flex-1 [&:nth-child(1_of_.fi-btn)]:rounded-s-lg [&:nth-last-child(1_of_.fi-btn)]:rounded-e-lg [&:not(:nth-child(1_of_.fi-btn))]:shadow-[-1px_0_0_0_theme(colors.gray.200)] [&:not(:nth-last-child(1_of_.fi-btn))]:me-px dark:[&:not(:nth-child(1_of_.fi-btn))]:shadow-[-1px_0_0_0_theme(colors.white/20%)]' => $grouped,
'cursor-pointer' => $tag === 'label',
match ($color) {
'gray' => null,
default => 'fi-color-custom',
},
// @deprecated `fi-btn-color-*` has been replaced by `fi-color-*` and `fi-color-custom`.
is_string($color) ? "fi-btn-color-{$color}" : null,
is_string($color) ? "fi-color-{$color}" : null,
($size instanceof ActionSize) ? "fi-size-{$size->value}" : null,
// @deprecated `fi-btn-size-*` has been replaced by `fi-size-*`.
($size instanceof ActionSize) ? "fi-btn-size-{$size->value}" : null,
match ($size) {
ActionSize::ExtraSmall => 'gap-1 px-2 py-1.5 text-xs',
ActionSize::Small => 'gap-1 px-2.5 py-1.5 text-sm',
ActionSize::Medium => 'gap-1.5 px-3 py-2 text-sm',
ActionSize::Large => 'gap-1.5 px-3.5 py-2.5 text-sm',
ActionSize::ExtraLarge => 'gap-1.5 px-4 py-3 text-sm',
default => $size,
},
'hidden' => $labeledFrom,
match ($labeledFrom) {
'sm' => 'sm:inline-grid',
'md' => 'md:inline-grid',
'lg' => 'lg:inline-grid',
'xl' => 'xl:inline-grid',
'2xl' => '2xl:inline-grid',
default => 'inline-grid',
},
],
...(
$outlined ?
[
'fi-btn-outlined ring-1',
match ($color) {
'gray' => 'text-gray-950 ring-gray-300 hover:bg-gray-400/10 focus-visible:ring-gray-400/40 dark:text-white dark:ring-gray-700',
default => 'text-custom-600 ring-custom-600 hover:bg-custom-400/10 dark:text-custom-400 dark:ring-custom-500',
},
] :
[
'shadow-sm' => ! $grouped,
'bg-white text-gray-950 hover:bg-gray-50 dark:bg-white/5 dark:text-white dark:hover:bg-white/10' => ($color === 'gray') || ($tag === 'label'),
'ring-1 ring-gray-950/10 dark:ring-white/20' => (($color === 'gray') || ($tag === 'label')) && (! $grouped),
'bg-custom-600 text-white hover:bg-custom-500 focus-visible:ring-custom-500/50 dark:bg-custom-500 dark:hover:bg-custom-400 dark:focus-visible:ring-custom-400/50' => ($color !== 'gray') && ($tag !== 'label'),
'[input:checked+&]:bg-custom-600 [input:checked+&]:text-white [input:checked+&]:ring-0 [input:checked+&]:hover:bg-custom-500 dark:[input:checked+&]:bg-custom-500 dark:[input:checked+&]:hover:bg-custom-400 [input:checked:focus-visible+&]:ring-custom-500/50 dark:[input:checked:focus-visible+&]:ring-custom-400/50 [input:focus-visible+&]:z-10 [input:focus-visible+&]:ring-2 [input:focus-visible+&]:ring-gray-950/10 dark:[input:focus-visible+&]:ring-white/20' => ($color !== 'gray') && ($tag === 'label'),
'[input:checked+&]:bg-gray-400 [input:checked+&]:text-white [input:checked+&]:ring-0 [input:checked+&]:hover:bg-gray-300 dark:[input:checked+&]:bg-gray-600 dark:[input:checked+&]:hover:bg-gray-500' => ($color === 'gray'),
]
),
]);
$buttonStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 500, 600],
alias: 'button',
) => $color !== 'gray',
]);
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-btn-icon transition duration-75',
match ($iconSize) {
IconSize::Small => 'h-4 w-4',
IconSize::Medium => 'h-5 w-5',
IconSize::Large => 'h-6 w-6',
default => $iconSize,
},
'text-gray-400 dark:text-gray-500' => ($color === 'gray') || ($tag === 'label'),
'text-white' => ($color !== 'gray') && ($tag !== 'label') && (! $outlined),
'[:checked+*>&]:text-white' => $tag === 'label',
]);
$badgeContainerClasses = 'fi-btn-badge-ctn absolute start-full top-0 z-[1] w-max -translate-x-1/2 -translate-y-1/2 rounded-md bg-white dark:bg-gray-900 rtl:translate-x-1/2';
$labelClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-btn-label',
'sr-only' => $labelSrOnly,
]);
$wireTarget = $loadingIndicator ? $attributes->whereStartsWith(['wire:target', 'wire:click'])->filter(fn ($value): bool => filled($value))->first() : null;
$hasFormProcessingLoadingIndicator = $type === 'submit' && filled($form);
$hasLoadingIndicator = filled($wireTarget) || $hasFormProcessingLoadingIndicator;
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget ?: $form, ENT_QUOTES);
}
$hasTooltip = filled($tooltip);
@endphp
@if ($labeledFrom)
<x-filament::icon-button
:badge="$badge"
:badge-color="$badgeColor"
:color="$color"
:disabled="$disabled"
:form="$form"
:form-id="$formId"
:href="$href"
:icon="$icon"
:icon-alias="$iconAlias"
:icon-size="$iconSize"
:key-bindings="$keyBindings"
:label="$slot"
:size="$size"
:tag="$tag"
:target="$target"
:tooltip="$tooltip"
:type="$type"
:class="
match ($labeledFrom) {
'sm' => 'sm:hidden',
'md' => 'md:hidden',
'lg' => 'lg:hidden',
'xl' => 'xl:hidden',
'2xl' => '2xl:hidden',
default => 'hidden',
}
"
:attributes="\Filament\Support\prepare_inherited_attributes($attributes)"
/>
@endif
<{{ $tag }}
@if ($tag === 'a')
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@endif
@if (($keyBindings || $hasTooltip) && (! $hasFormProcessingLoadingIndicator))
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
@if ($hasFormProcessingLoadingIndicator)
x-data="{
form: null,
isProcessing: false,
processingMessage: null,
}"
x-init="
form = $el.closest('form')
form?.addEventListener('form-processing-started', (event) => {
isProcessing = true
processingMessage = event.detail.message
})
form?.addEventListener('form-processing-finished', () => {
isProcessing = false
})
"
x-bind:class="{ 'enabled:opacity-70 enabled:cursor-wait': isProcessing }"
@endif
{{
$attributes
->merge([
'disabled' => $disabled,
'form' => $formId,
'type' => $tag === 'button' ? $type : null,
'wire:loading.attr' => $tag === 'button' ? 'disabled' : null,
'wire:target' => ($hasLoadingIndicator && $loadingIndicatorTarget) ? $loadingIndicatorTarget : null,
'x-bind:disabled' => $hasFormProcessingLoadingIndicator ? 'isProcessing' : null,
], escape: false)
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($iconPosition === IconPosition::Before)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)->class([$iconClasses])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)->class([$iconClasses])
"
/>
@endif
@if ($hasFormProcessingLoadingIndicator)
<x-filament::loading-indicator
x-cloak="x-cloak"
x-show="isProcessing"
:class="$iconClasses"
/>
@endif
@endif
<span
@if ($hasFormProcessingLoadingIndicator)
x-show="! isProcessing"
@endif
class="{{ $labelClasses }}"
>
{{ $slot }}
</span>
@if ($hasFormProcessingLoadingIndicator)
<span
x-cloak
x-show="isProcessing"
x-text="processingMessage"
class="{{ $labelClasses }}"
></span>
@endif
@if ($iconPosition === IconPosition::After)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)->class([$iconClasses])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)->class([$iconClasses])
"
/>
@endif
@if ($hasFormProcessingLoadingIndicator)
<x-filament::loading-indicator
x-cloak="x-cloak"
x-show="isProcessing"
:class="$iconClasses"
/>
@endif
@endif
@if (filled($badge))
<div class="{{ $badgeContainerClasses }}">
<x-filament::badge :color="$badgeColor" :size="$badgeSize">
{{ $badge }}
</x-filament::badge>
</div>
@endif
</{{ $tag }}>

View File

@ -0,0 +1,3 @@
<x-filament::section>
{{ $slot }}
</x-filament::section>

View File

@ -0,0 +1,71 @@
@php
use Filament\Support\Enums\IconSize;
@endphp
@props([
'color' => 'gray',
'icon' => null,
'iconSize' => IconSize::Medium,
'tag' => 'div',
])
<{{ $tag }}
{{
$attributes
->class([
'fi-dropdown-header flex w-full gap-2 p-3 text-sm',
match ($color) {
'gray' => null,
default => 'fi-color-custom',
},
// @deprecated `fi-dropdown-header-color-*` has been replaced by `fi-color-*` and `fi-color-custom`.
is_string($color) ? "fi-dropdown-header-color-{$color}" : null,
is_string($color) ? "fi-color-{$color}" : null,
])
}}
>
@if (filled($icon))
<x-filament::icon
:icon="$icon"
@class([
'fi-dropdown-header-icon',
match ($iconSize) {
IconSize::Small, 'sm' => 'h-4 w-4',
IconSize::Medium, 'md' => 'h-5 w-5',
IconSize::Large, 'lg' => 'h-6 w-6',
default => $iconSize,
},
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500 dark:text-custom-400',
},
])
@style([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 500],
alias: 'dropdown.header.icon',
) => $color !== 'gray',
])
/>
@endif
<span
@class([
'fi-dropdown-header-label flex-1 truncate text-start',
match ($color) {
'gray' => 'text-gray-700 dark:text-gray-200',
default => 'text-custom-600 dark:text-custom-400',
},
])
@style([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 600],
alias: 'dropdown.header.label',
) => $color !== 'gray',
])
>
{{ $slot }}
</span>
</{{ $tag }}>

View File

@ -0,0 +1,87 @@
@props([
'availableHeight' => null,
'availableWidth' => null,
'flip' => true,
'maxHeight' => null,
'offset' => 8,
'placement' => null,
'shift' => false,
'size' => false,
'sizePadding' => 16,
'teleport' => false,
'trigger' => null,
'width' => null,
])
@php
use Filament\Support\Enums\MaxWidth;
$sizeConfig = collect([
'availableHeight' => $availableHeight,
'availableWidth' => $availableWidth,
'padding' => $sizePadding,
])->filter()->toJson();
@endphp
<div
x-data="{
toggle: function (event) {
$refs.panel.toggle(event)
},
open: function (event) {
$refs.panel.open(event)
},
close: function (event) {
$refs.panel.close(event)
},
}"
{{ $attributes->class(['fi-dropdown']) }}
>
<div
x-on:click="toggle"
{{ $trigger->attributes->class(['fi-dropdown-trigger flex cursor-pointer']) }}
>
{{ $trigger }}
</div>
@if (! \Filament\Support\is_slot_empty($slot))
<div
x-cloak
x-float{{ $placement ? ".placement.{$placement}" : '' }}{{ $size ? '.size' : '' }}{{ $flip ? '.flip' : '' }}{{ $shift ? '.shift' : '' }}{{ $teleport ? '.teleport' : '' }}{{ $offset ? '.offset' : '' }}="{ offset: {{ $offset }}, {{ $size ? ('size: ' . $sizeConfig) : '' }} }"
x-ref="panel"
x-transition:enter-start="opacity-0"
x-transition:leave-end="opacity-0"
@if ($attributes->has('wire:key'))
wire:ignore.self
wire:key="{{ $attributes->get('wire:key') }}.panel"
@endif
@class([
'fi-dropdown-panel absolute z-10 w-screen divide-y divide-gray-100 rounded-lg bg-white shadow-lg ring-1 ring-gray-950/5 transition dark:divide-white/5 dark:bg-gray-900 dark:ring-white/10',
match ($width) {
// These max width classes need to be `!important` otherwise they will be usurped by the Floating UI "size" middleware.
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',
null => '!max-w-[14rem]',
default => $width,
},
'overflow-y-auto' => $maxHeight || $size,
])
@style([
"max-height: {$maxHeight}" => $maxHeight,
])
>
{{ $slot }}
</div>
@endif
</div>

View File

@ -0,0 +1,3 @@
<div {{ $attributes->class(['fi-dropdown-list p-1']) }}>
{{ $slot }}
</div>

View File

@ -0,0 +1,275 @@
@php
use Filament\Support\Enums\IconSize;
@endphp
@props([
'badge' => null,
'badgeColor' => null,
'badgeTooltip' => null,
'color' => 'gray',
'disabled' => false,
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconColor' => null,
'iconSize' => IconSize::Medium,
'image' => null,
'keyBindings' => null,
'loadingIndicator' => true,
'spaMode' => null,
'tag' => 'button',
'target' => null,
'tooltip' => null,
])
@php
$buttonClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-dropdown-list-item flex w-full items-center gap-2 whitespace-nowrap rounded-md p-2 text-sm transition-colors duration-75 outline-none disabled:pointer-events-none disabled:opacity-70',
'pointer-events-none opacity-70' => $disabled,
match ($color) {
'gray' => 'hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5',
default => 'fi-color-custom hover:bg-custom-50 focus-visible:bg-custom-50 dark:hover:bg-custom-400/10 dark:focus-visible:bg-custom-400/10',
},
// @deprecated `fi-dropdown-list-item-color-*` has been replaced by `fi-color-*` and `fi-color-custom`.
is_string($color) ? "fi-dropdown-list-item-color-{$color}" : null,
is_string($color) ? "fi-color-{$color}" : null,
]);
$buttonStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [50, 400],
alias: 'dropdown.list.item',
) => $color !== 'gray',
]);
$iconColor ??= $color;
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-dropdown-list-item-icon',
match ($iconSize) {
IconSize::Small, 'sm' => 'h-4 w-4',
IconSize::Medium, 'md' => 'h-5 w-5',
IconSize::Large, 'lg' => 'h-6 w-6',
default => $iconSize,
},
match ($iconColor) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500 dark:text-custom-400',
},
]);
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$iconColor,
shades: [400, 500],
alias: 'dropdown.list.item.icon',
) => $iconColor !== 'gray',
]);
$imageClasses = 'fi-dropdown-list-item-image h-5 w-5 rounded-full bg-cover bg-center';
$labelClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-dropdown-list-item-label flex-1 truncate text-start',
match ($color) {
'gray' => 'text-gray-700 dark:text-gray-200',
default => 'text-custom-600 dark:text-custom-400 ',
},
]);
$labelStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 600],
alias: 'dropdown.list.item.label',
) => $color !== 'gray',
]);
$wireTarget = $loadingIndicator ? $attributes->whereStartsWith(['wire:target', 'wire:click'])->filter(fn ($value): bool => filled($value))->first() : null;
$hasLoadingIndicator = filled($wireTarget);
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget, ENT_QUOTES);
}
$hasTooltip = filled($tooltip);
@endphp
@if ($tag === 'button')
<button
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->merge([
'disabled' => $disabled,
'type' => 'button',
'wire:loading.attr' => 'disabled',
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
], escape: false)
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@if ($image)
<div
class="{{ $imageClasses }}"
style="background-image: url('{{ $image }}')"
></div>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
<span class="{{ $labelClasses }}" style="{{ $labelStyles }}">
{{ $slot }}
</span>
@if (filled($badge))
<x-filament::badge
:color="$badgeColor"
size="sm"
:tooltip="$badgeTooltip"
>
{{ $badge }}
</x-filament::badge>
@endif
</button>
@elseif ($tag === 'a')
<a
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($icon)
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
@if ($image)
<div
class="{{ $imageClasses }}"
style="background-image: url('{{ $image }}')"
></div>
@endif
<span class="{{ $labelClasses }}" style="{{ $labelStyles }}">
{{ $slot }}
</span>
@if (filled($badge))
<x-filament::badge :color="$badgeColor" size="sm">
{{ $badge }}
</x-filament::badge>
@endif
</a>
@elseif ($tag === 'form')
<form
{{ $attributes->only(['action', 'class', 'method', 'wire:submit']) }}
>
@csrf
<button
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
type="submit"
{{
$attributes
->except(['action', 'class', 'method', 'wire:submit'])
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($icon)
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
<span class="{{ $labelClasses }}" style="{{ $labelStyles }}">
{{ $slot }}
</span>
@if (filled($badge))
<x-filament::badge :color="$badgeColor" size="sm">
{{ $badge }}
</x-filament::badge>
@endif
</button>
</form>
@endif

View File

@ -0,0 +1,25 @@
@props([
'label' => null,
'labelHidden' => false,
])
<fieldset
{{
$attributes->class([
'fi-fieldset rounded-xl border border-gray-200 p-6 dark:border-white/10',
])
}}
>
@if (filled($label))
<legend
@class([
'-ms-2 px-2 text-sm font-medium leading-6 text-gray-950 dark:text-white',
'sr-only' => $labelHidden,
])
>
{{ $label }}
</legend>
@endif
{{ $slot }}
</fieldset>

View File

@ -0,0 +1,62 @@
@props([
'default' => 1,
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'twoXl' => null,
'defaultStart' => null,
'smStart' => null,
'mdStart' => null,
'lgStart' => null,
'xlStart' => null,
'twoXlStart' => null,
'hidden' => false,
])
@php
$getSpanValue = function ($span): string {
if ($span === 'full') {
return '1 / -1';
}
return "span {$span} / span {$span}";
};
@endphp
<div
{{
$attributes
->class([
'hidden' => $hidden || $default === 'hidden',
'col-[--col-span-default]' => $default && (! $hidden),
'sm:col-[--col-span-sm]' => $sm && (! $hidden),
'md:col-[--col-span-md]' => $md && (! $hidden),
'lg:col-[--col-span-lg]' => $lg && (! $hidden),
'xl:col-[--col-span-xl]' => $xl && (! $hidden),
'2xl:col-[--col-span-2xl]' => $twoXl && (! $hidden),
'col-start-[--col-start-default]' => $defaultStart && (! $hidden),
'sm:col-start-[--col-start-sm]' => $smStart && (! $hidden),
'md:col-start-[--col-start-md]' => $mdStart && (! $hidden),
'lg:col-start-[--col-start-lg]' => $lgStart && (! $hidden),
'xl:col-start-[--col-start-xl]' => $xlStart && (! $hidden),
'2xl:col-start-[--col-start-2xl]' => $twoXlStart && (! $hidden),
])
->style([
"--col-span-default: {$getSpanValue($default)}" => $default,
"--col-span-sm: {$getSpanValue($sm)}" => $sm,
"--col-span-md: {$getSpanValue($md)}" => $md,
"--col-span-lg: {$getSpanValue($lg)}" => $lg,
"--col-span-xl: {$getSpanValue($xl)}" => $xl,
"--col-span-2xl: {$getSpanValue($twoXl)}" => $twoXl,
"--col-start-default: {$defaultStart}" => $defaultStart,
"--col-start-sm: {$smStart}" => $smStart,
"--col-start-md: {$mdStart}" => $mdStart,
"--col-start-lg: {$lgStart}" => $lgStart,
"--col-start-xl: {$xlStart}" => $xlStart,
"--col-start-2xl: {$twoXlStart}" => $twoXlStart,
])
}}
>
{{ $slot }}
</div>

View File

@ -0,0 +1,53 @@
@props([
'isGrid' => true,
'default' => 1,
'direction' => 'row',
'sm' => null,
'md' => null,
'lg' => null,
'xl' => null,
'twoXl' => null,
])
<div
{{
$attributes
->class([
'grid' => $isGrid && $direction === 'row',
'grid-cols-[--cols-default]' => $default && ($direction === 'row'),
'columns-[--cols-default]' => $default && ($direction === 'column'),
'sm:grid-cols-[--cols-sm]' => $sm && ($direction === 'row'),
'sm:columns-[--cols-sm]' => $sm && ($direction === 'column'),
'md:grid-cols-[--cols-md]' => $md && ($direction === 'row'),
'md:columns-[--cols-md]' => $md && ($direction === 'column'),
'lg:grid-cols-[--cols-lg]' => $lg && ($direction === 'row'),
'lg:columns-[--cols-lg]' => $lg && ($direction === 'column'),
'xl:grid-cols-[--cols-xl]' => $xl && ($direction === 'row'),
'xl:columns-[--cols-xl]' => $xl && ($direction === 'column'),
'2xl:grid-cols-[--cols-2xl]' => $twoXl && ($direction === 'row'),
'2xl:columns-[--cols-2xl]' => $twoXl && ($direction === 'column'),
])
->style(
match ($direction) {
'column' => [
"--cols-default: {$default}" => $default,
"--cols-sm: {$sm}" => $sm,
"--cols-md: {$md}" => $md,
"--cols-lg: {$lg}" => $lg,
"--cols-xl: {$xl}" => $xl,
"--cols-2xl: {$twoXl}" => $twoXl,
],
'row' => [
"--cols-default: repeat({$default}, minmax(0, 1fr))" => $default,
"--cols-sm: repeat({$sm}, minmax(0, 1fr))" => $sm,
"--cols-md: repeat({$md}, minmax(0, 1fr))" => $md,
"--cols-lg: repeat({$lg}, minmax(0, 1fr))" => $lg,
"--cols-xl: repeat({$xl}, minmax(0, 1fr))" => $xl,
"--cols-2xl: repeat({$twoXl}, minmax(0, 1fr))" => $twoXl,
],
},
)
}}
>
{{ $slot }}
</div>

View File

@ -0,0 +1,242 @@
@php
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\IconSize;
@endphp
@props([
'badge' => null,
'badgeColor' => 'primary',
'badgeSize' => 'xs',
'color' => 'primary',
'disabled' => false,
'form' => null,
'formId' => null,
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconSize' => null,
'keyBindings' => null,
'label' => null,
'loadingIndicator' => true,
'size' => ActionSize::Medium,
'spaMode' => null,
'tag' => 'button',
'target' => null,
'tooltip' => null,
'type' => 'button',
])
@php
if (! $size instanceof ActionSize) {
$size = filled($size) ? (ActionSize::tryFrom($size) ?? $size) : null;
}
$iconSize ??= match ($size) {
ActionSize::ExtraSmall => IconSize::Small,
ActionSize::Small, ActionSize::Medium => IconSize::Medium,
ActionSize::Large, ActionSize::ExtraLarge => IconSize::Large,
default => IconSize::Medium,
};
if (! $iconSize instanceof IconSize) {
$iconSize = filled($iconSize) ? (IconSize::tryFrom($iconSize) ?? $iconSize) : null;
}
$buttonClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-icon-btn relative flex items-center justify-center rounded-lg outline-none transition duration-75 focus-visible:ring-2',
'pointer-events-none opacity-70' => $disabled,
...match ($size) {
ActionSize::ExtraSmall => [
match ($iconSize) {
IconSize::Small => '-m-1.5',
IconSize::Medium => '-m-1',
IconSize::Large => '-m-0.5',
},
'h-7 w-7',
],
ActionSize::Small => [
match ($iconSize) {
IconSize::Small => '-m-2',
IconSize::Medium => '-m-1.5',
IconSize::Large => '-m-1',
},
'h-8 w-8',
],
ActionSize::Medium => [
match ($iconSize) {
IconSize::Small => '-m-2.5',
IconSize::Medium => '-m-2',
IconSize::Large => '-m-1.5',
},
'h-9 w-9',
],
ActionSize::Large => [
match ($iconSize) {
IconSize::Small => '-m-3',
IconSize::Medium => '-m-2.5',
IconSize::Large => '-m-2',
},
'h-10 w-10',
],
ActionSize::ExtraLarge => [
match ($iconSize) {
IconSize::Small => '-m-3.5',
IconSize::Medium => '-m-3',
IconSize::Large => '-m-2.5',
},
'h-11 w-11',
],
},
match ($color) {
'gray' => 'text-gray-400 hover:text-gray-500 focus-visible:ring-primary-600 dark:text-gray-500 dark:hover:text-gray-400 dark:focus-visible:ring-primary-500',
default => 'fi-color-custom text-custom-500 hover:text-custom-600 focus-visible:ring-custom-600 dark:text-custom-400 dark:hover:text-custom-300 dark:focus-visible:ring-custom-500',
},
is_string($color) ? "fi-color-{$color}" : null,
]);
$buttonStyles = \Filament\Support\get_color_css_variables(
$color,
shades: [300, 400, 500, 600],
alias: 'icon-button',
);
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-icon-btn-icon',
match ($iconSize) {
IconSize::Small => 'h-4 w-4',
IconSize::Medium => 'h-5 w-5',
IconSize::Large => 'h-6 w-6',
default => $iconSize,
},
]);
$badgeContainerClasses = 'fi-icon-btn-badge-ctn absolute start-full top-1 z-[1] w-max -translate-x-1/2 -translate-y-1/2 rounded-md bg-white dark:bg-gray-900 rtl:translate-x-1/2';
$wireTarget = $loadingIndicator ? $attributes->whereStartsWith(['wire:target', 'wire:click'])->filter(fn ($value): bool => filled($value))->first() : null;
$hasLoadingIndicator = filled($wireTarget) || ($type === 'submit' && filled($form));
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget ?: $form, ENT_QUOTES);
}
$hasTooltip = filled($tooltip);
@endphp
@if ($tag === 'button')
<button
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->merge([
'disabled' => $disabled,
'form' => $formId,
'type' => $type,
'wire:loading.attr' => 'disabled',
'wire:target' => ($hasLoadingIndicator && $loadingIndicatorTarget) ? $loadingIndicatorTarget : null,
], escape: false)
->merge([
'title' => $hasTooltip ? null : $label,
], escape: true)
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($label)
<span class="sr-only">
{{ $label }}
</span>
@endif
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)->class([$iconClasses])
"
/>
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)->class([$iconClasses])
"
/>
@endif
@if (filled($badge))
<div class="{{ $badgeContainerClasses }}">
<x-filament::badge :color="$badgeColor" :size="$badgeSize">
{{ $badge }}
</x-filament::badge>
</div>
@endif
</button>
@elseif ($tag === 'a')
<a
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->merge([
'title' => $hasTooltip ? null : $label,
], escape: true)
->class([$buttonClasses])
->style([$buttonStyles])
}}
>
@if ($label)
<span class="sr-only">
{{ $label }}
</span>
@endif
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
:class="$iconClasses"
/>
@if (filled($badge))
<div class="{{ $badgeContainerClasses }}">
<x-filament::badge :color="$badgeColor" size="xs">
{{ $badge }}
</x-filament::badge>
</div>
@endif
</a>
@endif

View File

@ -0,0 +1,29 @@
@props([
'alias' => null,
'class' => '',
'icon' => null,
])
@php
$icon = ($alias ? \Filament\Support\Facades\FilamentIcon::resolve($alias) : null) ?: ($icon ?? $slot);
@endphp
@if ($icon instanceof \Illuminate\Contracts\Support\Htmlable)
<span {{ $attributes->class($class) }}>
{{ $icon }}
</span>
@elseif (str_contains($icon, '/'))
<img
{{
$attributes
->merge(['src' => $icon])
->class($class)
}}
/>
@else
@svg(
$icon,
$class,
array_filter($attributes->getAttributes()),
)
@endif

View File

@ -0,0 +1,29 @@
@props([
'alpineValid' => null,
'valid' => true,
])
@php
$hasAlpineValidClasses = filled($alpineValid);
$validInputClasses = 'text-primary-600 ring-gray-950/10 focus:ring-primary-600 checked:focus:ring-primary-500/50 dark:text-primary-500 dark:ring-white/20 dark:checked:bg-primary-500 dark:focus:ring-primary-500 dark:checked:focus:ring-primary-400/50 dark:disabled:ring-white/10';
$invalidInputClasses = 'fi-invalid text-danger-600 ring-danger-600 focus:ring-danger-600 checked:focus:ring-danger-500/50 dark:text-danger-500 dark:ring-danger-500 dark:checked:bg-danger-500 dark:focus:ring-danger-500 dark:checked:focus:ring-danger-400/50';
@endphp
<input
type="checkbox"
@if ($hasAlpineValidClasses)
x-bind:class="{
@js($validInputClasses): {{ $alpineValid }},
@js($invalidInputClasses): {{ "(! {$alpineValid})" }},
}"
@endif
{{
$attributes
->class([
'fi-checkbox-input rounded border-none bg-white shadow-sm ring-1 transition duration-75 checked:ring-0 focus:ring-2 focus:ring-offset-0 disabled:pointer-events-none disabled:bg-gray-50 disabled:text-gray-50 disabled:checked:bg-gray-400 disabled:checked:text-gray-400 dark:bg-white/5 dark:disabled:bg-transparent dark:disabled:checked:bg-gray-600',
$validInputClasses => (! $hasAlpineValidClasses) && $valid,
$invalidInputClasses => (! $hasAlpineValidClasses) && (! $valid),
])
}}
/>

View File

@ -0,0 +1,22 @@
@props([
'inlinePrefix' => false,
'inlineSuffix' => false,
])
<input
{{
$attributes->class([
'fi-input block w-full border-none py-1.5 text-base text-gray-950 transition duration-75 placeholder:text-gray-400 focus:ring-0 disabled:text-gray-500 disabled:[-webkit-text-fill-color:theme(colors.gray.500)] disabled:placeholder:[-webkit-text-fill-color:theme(colors.gray.400)] dark:text-white dark:placeholder:text-gray-500 dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:theme(colors.gray.400)] dark:disabled:placeholder:[-webkit-text-fill-color:theme(colors.gray.500)] sm:text-sm sm:leading-6',
// A fully transparent white background color is used
// instead of transparent to fix a Safari bug
// where the date/time input "placeholder" colors too dark.
//
// https://github.com/filamentphp/filament/issues/7087
'bg-white/0',
'ps-0' => $inlinePrefix,
'ps-3' => ! $inlinePrefix,
'pe-0' => $inlineSuffix,
'pe-3' => ! $inlineSuffix,
])
}}
/>

View File

@ -0,0 +1,15 @@
@props([
'valid' => true,
])
<input
type="radio"
{{
$attributes
->class([
'fi-radio-input border-none bg-white shadow-sm ring-1 transition duration-75 checked:ring-0 focus:ring-2 focus:ring-offset-0 disabled:bg-gray-50 disabled:text-gray-50 disabled:checked:bg-gray-400 disabled:checked:text-gray-400 dark:bg-white/5 dark:disabled:bg-transparent dark:disabled:checked:bg-gray-600',
'text-primary-600 ring-gray-950/10 focus:ring-primary-600 checked:focus:ring-primary-500/50 dark:text-primary-500 dark:ring-white/20 dark:checked:bg-primary-500 dark:focus:ring-primary-500 dark:checked:focus:ring-primary-400/50 dark:disabled:ring-white/10' => $valid,
'fi-invalid text-danger-600 ring-danger-600 focus:ring-danger-600 checked:focus:ring-danger-500/50 dark:text-danger-500 dark:ring-danger-500 dark:checked:bg-danger-500 dark:focus:ring-danger-500 dark:checked:focus:ring-danger-400/50' => ! $valid,
])
}}
/>

View File

@ -0,0 +1,16 @@
@props([
'inlinePrefix' => false,
'inlineSuffix' => false,
])
<select
{{
$attributes->class([
'fi-select-input block w-full border-none bg-transparent py-1.5 pe-8 text-base text-gray-950 transition duration-75 focus:ring-0 disabled:text-gray-500 disabled:[-webkit-text-fill-color:theme(colors.gray.500)] dark:text-white dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:theme(colors.gray.400)] sm:text-sm sm:leading-6 [&_optgroup]:bg-white [&_optgroup]:dark:bg-gray-900 [&_option]:bg-white [&_option]:dark:bg-gray-900',
'ps-0' => $inlinePrefix,
'ps-3' => ! $inlinePrefix,
])
}}
>
{{ $slot }}
</select>

View File

@ -0,0 +1,212 @@
@props([
'alpineDisabled' => null,
'alpineValid' => null,
'disabled' => false,
'inlinePrefix' => false,
'inlineSuffix' => false,
'prefix' => null,
'prefixActions' => [],
'prefixIcon' => null,
'prefixIconColor' => 'gray',
'prefixIconAlias' => null,
'suffix' => null,
'suffixActions' => [],
'suffixIcon' => null,
'suffixIconColor' => 'gray',
'suffixIconAlias' => null,
'valid' => true,
])
@php
$prefixActions = array_filter(
$prefixActions,
fn (\Filament\Forms\Components\Actions\Action $prefixAction): bool => $prefixAction->isVisible(),
);
$suffixActions = array_filter(
$suffixActions,
fn (\Filament\Forms\Components\Actions\Action $suffixAction): bool => $suffixAction->isVisible(),
);
$hasPrefix = count($prefixActions) || $prefixIcon || filled($prefix);
$hasSuffix = count($suffixActions) || $suffixIcon || filled($suffix);
$hasAlpineDisabledClasses = filled($alpineDisabled);
$hasAlpineValidClasses = filled($alpineValid);
$hasAlpineClasses = $hasAlpineDisabledClasses || $hasAlpineValidClasses;
$enabledWrapperClasses = 'bg-white dark:bg-white/5 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-2';
$disabledWrapperClasses = 'fi-disabled bg-gray-50 dark:bg-transparent';
$validWrapperClasses = 'ring-gray-950/10';
$invalidWrapperClasses = 'fi-invalid ring-danger-600 dark:ring-danger-500';
$enabledValidWrapperClasses = 'dark:ring-white/20 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-600 dark:[&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-500';
$enabledInvalidWrapperClasses = '[&:not(:has(.fi-ac-action:focus))]:focus-within:ring-danger-600 dark:[&:not(:has(.fi-ac-action:focus))]:focus-within:ring-danger-500';
$disabledValidWrapperClasses = 'dark:ring-white/10';
$actionsClasses = 'flex items-center gap-3';
$labelClasses = 'fi-input-wrp-label whitespace-nowrap text-sm text-gray-500 dark:text-gray-400';
$getIconClasses = fn (string | array $color = 'gray'): string => \Illuminate\Support\Arr::toCssClasses([
'fi-input-wrp-icon h-5 w-5',
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-500',
},
]);
$getIconStyles = fn (string | array $color = 'gray'): string => \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [500],
alias: 'input-wrapper.icon',
) => $color !== 'gray',
]);
$wireTarget = $attributes->whereStartsWith(['wire:target'])->first();
$hasLoadingIndicator = filled($wireTarget);
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget, ENT_QUOTES);
}
@endphp
<div
@if ($hasAlpineClasses)
x-bind:class="{
{{ $hasAlpineDisabledClasses ? "'{$enabledWrapperClasses}': ! ({$alpineDisabled})," : null }}
{{ $hasAlpineDisabledClasses ? "'{$disabledWrapperClasses}': {$alpineDisabled}," : null }}
{{ $hasAlpineValidClasses ? "'{$validWrapperClasses}': {$alpineValid}," : null }}
{{ $hasAlpineValidClasses ? "'{$invalidWrapperClasses}': ! ({$alpineValid})," : null }}
{{ ($hasAlpineDisabledClasses && $hasAlpineValidClasses) ? "'{$enabledValidWrapperClasses}': ! ({$alpineDisabled}) && {$alpineValid}," : null }}
{{ ($hasAlpineDisabledClasses && $hasAlpineValidClasses) ? "'{$enabledInvalidWrapperClasses}': ! ({$alpineDisabled}) && ! ({$alpineValid})," : null }}
{{ ($hasAlpineDisabledClasses && $hasAlpineValidClasses) ? "'{$disabledValidWrapperClasses}': {$alpineDisabled} && ! ({$alpineValid})," : null }}
}"
@endif
{{
$attributes
->except(['wire:target', 'tabindex'])
->class([
'fi-input-wrp flex rounded-lg shadow-sm ring-1 transition duration-75',
$enabledWrapperClasses => (! $hasAlpineClasses) && (! $disabled),
$disabledWrapperClasses => (! $hasAlpineClasses) && $disabled,
$validWrapperClasses => (! $hasAlpineClasses) && $valid,
$invalidWrapperClasses => (! $hasAlpineClasses) && (! $valid),
$enabledValidWrapperClasses => (! $hasAlpineClasses) && (! $disabled) && $valid,
$enabledInvalidWrapperClasses => (! $hasAlpineClasses) && (! $disabled) && (! $valid),
$disabledValidWrapperClasses => (! $hasAlpineClasses) && $disabled && $valid,
])
}}
>
@if ($hasPrefix || $hasLoadingIndicator)
<div
@if (! $hasPrefix)
wire:loading.delay.{{ config('filament.livewire_loading_delay', 'default') }}.flex
wire:target="{{ $loadingIndicatorTarget }}"
wire:key="{{ \Illuminate\Support\Str::random() }}" {{-- Makes sure the loading indicator gets hidden again. --}}
@endif
@class([
'fi-input-wrp-prefix items-center gap-x-3 ps-3',
'flex' => $hasPrefix,
'hidden' => ! $hasPrefix,
'pe-1' => $inlinePrefix && filled($prefix),
'pe-2' => $inlinePrefix && blank($prefix),
'border-e border-gray-200 pe-3 ps-3 dark:border-white/10' => ! $inlinePrefix,
])
>
@if (count($prefixActions))
<div class="{{ $actionsClasses }}">
@foreach ($prefixActions as $prefixAction)
{{ $prefixAction }}
@endforeach
</div>
@endif
@if ($prefixIcon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $prefixIconAlias,
'icon' => $prefixIcon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$getIconClasses($prefixIconColor)])
->style([$getIconStyles($prefixIconColor)])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => $hasPrefix,
'wire:target' => $hasPrefix ? $loadingIndicatorTarget : null,
])
)->class([$getIconClasses()])
"
/>
@endif
@if (filled($prefix))
<span class="{{ $labelClasses }}">
{{ $prefix }}
</span>
@endif
</div>
@endif
<div
@if ($hasLoadingIndicator && (! $hasPrefix))
@if ($inlinePrefix)
wire:loading.delay.{{ config('filament.livewire_loading_delay', 'default') }}.class.remove="ps-3"
@endif
wire:target="{{ $loadingIndicatorTarget }}"
@endif
@class([
'fi-input-wrp-input min-w-0 flex-1',
'ps-3' => $hasLoadingIndicator && (! $hasPrefix) && $inlinePrefix,
])
>
{{ $slot }}
</div>
@if ($hasSuffix)
<div
@class([
'fi-input-wrp-suffix flex items-center gap-x-3 pe-3',
'ps-1' => $inlineSuffix && filled($suffix),
'ps-2' => $inlineSuffix && blank($suffix),
'border-s border-gray-200 ps-3 dark:border-white/10' => ! $inlineSuffix,
])
>
@if (filled($suffix))
<span class="{{ $labelClasses }}">
{{ $suffix }}
</span>
@endif
@if ($suffixIcon)
<x-filament::icon
:alias="$suffixIconAlias"
:icon="$suffixIcon"
:class="$getIconClasses($suffixIconColor)"
:style="$getIconStyles($suffixIconColor)"
/>
@endif
@if (count($suffixActions))
<div class="{{ $actionsClasses }}">
@foreach ($suffixActions as $suffixAction)
{{ $suffixAction }}
@endforeach
</div>
@endif
</div>
@endif
</div>

View File

@ -0,0 +1,304 @@
@php
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\FontWeight;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
@endphp
@props([
'badge' => null,
'badgeColor' => 'primary',
'badgeSize' => 'xs',
'color' => 'primary',
'disabled' => false,
'form' => null,
'formId' => null,
'href' => null,
'icon' => null,
'iconAlias' => null,
'iconPosition' => IconPosition::Before,
'iconSize' => null,
'keyBindings' => null,
'labelSrOnly' => false,
'loadingIndicator' => true,
'size' => ActionSize::Medium,
'spaMode' => null,
'tag' => 'a',
'target' => null,
'tooltip' => null,
'type' => 'button',
'weight' => FontWeight::SemiBold,
])
@php
if (! $iconPosition instanceof IconPosition) {
$iconPosition = filled($iconPosition) ? (IconPosition::tryFrom($iconPosition) ?? $iconPosition) : null;
}
if (! $size instanceof ActionSize) {
$size = filled($size) ? (ActionSize::tryFrom($size) ?? $size) : null;
}
$iconSize ??= match ($size) {
ActionSize::ExtraSmall, ActionSize::Small => IconSize::Small,
default => IconSize::Medium,
};
if (! $iconSize instanceof IconSize) {
$iconSize = filled($iconSize) ? (IconSize::tryFrom($iconSize) ?? $iconSize) : null;
}
$linkClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-link group/link relative inline-flex items-center justify-center outline-none',
'pointer-events-none opacity-70' => $disabled,
($size instanceof ActionSize) ? "fi-size-{$size->value}" : null,
// @deprecated `fi-link-size-*` has been replaced by `fi-size-*`.
($size instanceof ActionSize) ? "fi-link-size-{$size->value}" : null,
match ($size) {
ActionSize::ExtraSmall => 'gap-1',
ActionSize::Small => 'gap-1',
ActionSize::Medium => 'gap-1.5',
ActionSize::Large => 'gap-1.5',
ActionSize::ExtraLarge => 'gap-1.5',
default => $size,
},
match ($color) {
'gray' => null,
default => 'fi-color-custom',
},
is_string($color) ? "fi-color-{$color}" : null,
]);
if (! $labelSrOnly) {
$labelClasses = \Illuminate\Support\Arr::toCssClasses([
match ($weight) {
FontWeight::Thin, 'thin' => 'font-thin',
FontWeight::ExtraLight, 'extralight' => 'font-extralight',
FontWeight::Light, 'light' => 'font-light',
FontWeight::Medium, 'medium' => 'font-medium',
FontWeight::Normal, 'normal' => 'font-normal',
FontWeight::SemiBold, 'semibold' => 'font-semibold',
FontWeight::Bold, 'bold' => 'font-bold',
FontWeight::ExtraBold, 'extrabold' => 'font-extrabold',
FontWeight::Black, 'black' => 'font-black',
default => $weight,
},
match ($size) {
ActionSize::ExtraSmall => 'text-xs',
ActionSize::Small => 'text-sm',
ActionSize::Medium => 'text-sm',
ActionSize::Large => 'text-sm',
ActionSize::ExtraLarge => 'text-sm',
default => null,
},
match ($color) {
'gray' => 'text-gray-700 dark:text-gray-200',
default => 'text-custom-600 dark:text-custom-400',
},
'group-hover/link:underline group-focus-visible/link:underline',
]);
} else {
$labelClasses = 'sr-only';
}
$labelStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 600],
alias: 'link.label',
) => $color !== 'gray',
]);
$iconClasses = \Illuminate\Support\Arr::toCssClasses([
'fi-link-icon',
match ($iconSize) {
IconSize::Small => 'h-4 w-4',
IconSize::Medium => 'h-5 w-5',
IconSize::Large => 'h-6 w-6',
default => $iconSize,
},
match ($color) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'text-custom-600 dark:text-custom-400',
},
]);
$iconStyles = \Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [400, 600],
alias: 'link.icon',
) => $color !== 'gray',
]);
$badgeContainerClasses = 'fi-link-badge-ctn absolute start-full top-0 z-[1] w-max -translate-x-1/4 -translate-y-3/4 rounded-md bg-white dark:bg-gray-900 rtl:translate-x-1/4';
$wireTarget = $loadingIndicator ? $attributes->whereStartsWith(['wire:target', 'wire:click'])->filter(fn ($value): bool => filled($value))->first() : null;
$hasLoadingIndicator = filled($wireTarget) || ($type === 'submit' && filled($form));
if ($hasLoadingIndicator) {
$loadingIndicatorTarget = html_entity_decode($wireTarget ?: $form, ENT_QUOTES);
}
$hasTooltip = filled($tooltip);
@endphp
@if ($tag === 'a')
<a
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{ $attributes->class([$linkClasses]) }}
>
@if ($icon && $iconPosition === IconPosition::Before)
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
<span class="{{ $labelClasses }}" style="{{ $labelStyles }}">
{{ $slot }}
</span>
@if ($icon && $iconPosition === IconPosition::After)
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
:class="$iconClasses"
:style="$iconStyles"
/>
@endif
@if (filled($badge))
<div class="{{ $badgeContainerClasses }}">
<x-filament::badge :color="$badgeColor" :size="$badgeSize">
{{ $badge }}
</x-filament::badge>
</div>
@endif
</a>
@trim
@elseif ($tag === 'button')
<button
@if ($keyBindings || $hasTooltip)
x-data="{}"
@endif
@if ($keyBindings)
x-bind:id="$id('key-bindings')"
x-mousetrap.global.{{ collect($keyBindings)->map(fn (string $keyBinding): string => str_replace('+', '-', $keyBinding))->implode('.') }}="document.getElementById($el.id).click()"
@endif
@if ($hasTooltip)
x-tooltip="{
content: @js($tooltip),
theme: $store.theme,
}"
@endif
{{
$attributes
->merge([
'disabled' => $disabled,
'form' => $formId,
'type' => $type,
'wire:loading.attr' => 'disabled',
'wire:target' => ($hasLoadingIndicator && $loadingIndicatorTarget) ? $loadingIndicatorTarget : null,
], escape: false)
->class([$linkClasses])
}}
>
@if ($iconPosition === IconPosition::Before)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@endif
<span class="{{ $labelClasses }}" style="{{ $labelStyles }}">
{{ $slot }}
</span>
@if ($iconPosition === IconPosition::After)
@if ($icon)
<x-filament::icon
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'alias' => $iconAlias,
'icon' => $icon,
'wire:loading.remove.delay.' . config('filament.livewire_loading_delay', 'default') => $hasLoadingIndicator,
'wire:target' => $hasLoadingIndicator ? $loadingIndicatorTarget : null,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@if ($hasLoadingIndicator)
<x-filament::loading-indicator
:attributes="
\Filament\Support\prepare_inherited_attributes(
new \Illuminate\View\ComponentAttributeBag([
'wire:loading.delay.' . config('filament.livewire_loading_delay', 'default') => '',
'wire:target' => $loadingIndicatorTarget,
])
)
->class([$iconClasses])
->style([$iconStyles])
"
/>
@endif
@endif
@if (filled($badge))
<div class="{{ $badgeContainerClasses }}">
<x-filament::badge :color="$badgeColor" :size="$badgeSize">
{{ $badge }}
</x-filament::badge>
</div>
@endif
</button>
@trim
@endif

View File

@ -0,0 +1,18 @@
<svg
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{{ $attributes->class(['animate-spin']) }}
>
<path
clip-rule="evenodd"
d="M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
fill-rule="evenodd"
fill="currentColor"
opacity="0.2"
></path>
<path
d="M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z"
fill="currentColor"
></path>
</svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@ -0,0 +1,33 @@
@php
if ((! isset($columnSpan)) || (! is_array($columnSpan))) {
$columnSpan = [
'default' => $columnSpan ?? null,
];
}
if ((! isset($columnStart)) || (! is_array($columnStart))) {
$columnStart = [
'default' => $columnStart ?? null,
];
}
$height ??= '8rem';
@endphp
<x-filament::grid.column
:default="$columnSpan['default'] ?? 1"
:sm="$columnSpan['sm'] ?? null"
:md="$columnSpan['md'] ?? null"
:lg="$columnSpan['lg'] ?? null"
:xl="$columnSpan['xl'] ?? null"
:twoXl="$columnSpan['2xl'] ?? null"
:defaultStart="$columnStart['default'] ?? null"
:smStart="$columnStart['sm'] ?? null"
:mdStart="$columnStart['md'] ?? null"
:lgStart="$columnStart['lg'] ?? null"
:xlStart="$columnStart['xl'] ?? null"
:twoXlStart="$columnStart['2xl'] ?? null"
class="fi-loading-section"
>
<x-filament::section class="animate-pulse" style="height: {{ $height }}" />
</x-filament::grid.column>

View File

@ -0,0 +1,5 @@
<p
{{ $attributes->class(['fi-modal-description text-sm text-gray-500 dark:text-gray-400']) }}
>
{{ $slot }}
</p>

View File

@ -0,0 +1,5 @@
<h2
{{ $attributes->class(['fi-modal-heading text-base font-semibold leading-6 text-gray-950 dark:text-white']) }}
>
{{ $slot }}
</h2>

View File

@ -0,0 +1,376 @@
@php
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Facades\FilamentView;
@endphp
@props([
'alignment' => Alignment::Start,
'ariaLabelledby' => null,
'autofocus' => \Filament\Support\View\Components\Modal::$isAutofocused,
'closeButton' => \Filament\Support\View\Components\Modal::$hasCloseButton,
'closeByClickingAway' => \Filament\Support\View\Components\Modal::$isClosedByClickingAway,
'closeByEscaping' => \Filament\Support\View\Components\Modal::$isClosedByEscaping,
'closeEventName' => 'close-modal',
'description' => null,
'displayClasses' => 'inline-block',
'extraModalWindowAttributeBag' => null,
'footer' => null,
'footerActions' => [],
'footerActionsAlignment' => Alignment::Start,
'header' => null,
'heading' => null,
'icon' => null,
'iconAlias' => null,
'iconColor' => 'primary',
'id' => null,
'openEventName' => 'open-modal',
'slideOver' => false,
'stickyFooter' => false,
'stickyHeader' => false,
'trigger' => null,
'visible' => true,
'width' => 'sm',
])
@php
$hasDescription = filled($description);
$hasFooter = (! \Filament\Support\is_slot_empty($footer)) || (is_array($footerActions) && count($footerActions)) || (! is_array($footerActions) && (! \Filament\Support\is_slot_empty($footerActions)));
$hasHeading = filled($heading);
$hasIcon = filled($icon);
$hasSlot = ! \Filament\Support\is_slot_empty($slot);
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
if (! $footerActionsAlignment instanceof Alignment) {
$footerActionsAlignment = filled($footerActionsAlignment) ? (Alignment::tryFrom($footerActionsAlignment) ?? $footerActionsAlignment) : null;
}
if (! $width instanceof MaxWidth) {
$width = filled($width) ? (MaxWidth::tryFrom($width) ?? $width) : null;
}
$closeEventHandler = filled($id) ? '$dispatch(' . \Illuminate\Support\Js::from($closeEventName) . ', { id: ' . \Illuminate\Support\Js::from($id) . ' })' : 'close()';
@endphp
<div
@if ($ariaLabelledby)
aria-labelledby="{{ $ariaLabelledby }}"
@elseif ($heading)
aria-labelledby="{{ "{$id}.heading" }}"
@endif
aria-modal="true"
role="dialog"
x-data="{
isOpen: false,
livewire: null,
close: function () {
this.isOpen = false
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-closed', { id: '{{ $id }}' }),
)
},
open: function () {
this.$nextTick(() => {
this.isOpen = true
@if (FilamentView::hasSpaMode())
this.$dispatch('ax-modal-opened')
@endif
this.$refs.modalContainer.dispatchEvent(
new CustomEvent('modal-opened', { id: '{{ $id }}' }),
)
})
},
}"
@if ($id)
x-on:{{ $closeEventName }}.window="if ($event.detail.id === '{{ $id }}') close()"
x-on:{{ $openEventName }}.window="if ($event.detail.id === '{{ $id }}') open()"
@endif
x-trap.noscroll{{ $autofocus ? '' : '.noautofocus' }}="isOpen"
x-bind:class="{
'fi-modal-open': isOpen,
}"
@class([
'fi-modal',
'fi-width-screen' => $width === MaxWidth::Screen,
$displayClasses,
])
>
@if ($trigger)
<div
x-on:click="open"
{{ $trigger->attributes->class(['fi-modal-trigger flex cursor-pointer']) }}
>
{{ $trigger }}
</div>
@endif
<div x-cloak x-show="isOpen">
<div
aria-hidden="true"
x-show="isOpen"
x-transition.duration.300ms.opacity
@class([
'fi-modal-close-overlay fixed inset-0 z-40 bg-gray-950/50 dark:bg-gray-950/75',
])
></div>
<div
@class([
'fixed inset-0 z-40',
'overflow-y-auto' => ! ($slideOver || ($width === MaxWidth::Screen)),
'cursor-pointer' => $closeByClickingAway,
])
>
<div
x-ref="modalContainer"
@if ($closeByClickingAway)
{{-- Ensure that the click element is not triggered from a user selecting text inside an input. --}}
x-on:click.self="
document.activeElement.selectionStart === undefined &&
document.activeElement.selectionEnd === undefined &&
{{ $closeEventHandler }}
"
@endif
{{
$attributes->class([
'relative grid min-h-full grid-rows-[1fr_auto_1fr] justify-items-center sm:grid-rows-[1fr_auto_3fr]',
'p-4' => ! ($slideOver || ($width === MaxWidth::Screen)),
])
}}
>
<div
x-data="{ isShown: false }"
x-init="
$nextTick(() => {
isShown = isOpen
$watch('isOpen', () => (isShown = isOpen))
})
"
@if ($closeByEscaping)
x-on:keydown.window.escape="{{ $closeEventHandler }}"
@endif
x-show="isShown"
x-transition:enter="duration-300"
x-transition:leave="duration-300"
@if ($width === MaxWidth::Screen)
@elseif ($slideOver)
x-transition:enter-start="translate-x-full rtl:-translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="translate-x-full rtl:-translate-x-full"
@else
x-transition:enter-start="scale-95 opacity-0"
x-transition:enter-end="scale-100 opacity-100"
x-transition:leave-start="scale-100 opacity-100"
x-transition:leave-end="scale-95 opacity-0"
@endif
{{
($extraModalWindowAttributeBag ?? new \Illuminate\View\ComponentAttributeBag)->class([
'fi-modal-window pointer-events-auto relative row-start-2 flex w-full cursor-default flex-col bg-white shadow-xl ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10',
'fi-modal-slide-over-window ms-auto overflow-y-auto' => $slideOver,
// Using an arbitrary value instead of the h-dvh class that was added in Tailwind CSS v3.4.0
// to ensure compatibility with custom themes that may use an older version of Tailwind CSS.
'h-[100dvh]' => $slideOver || ($width === MaxWidth::Screen),
'mx-auto rounded-xl' => ! ($slideOver || ($width === MaxWidth::Screen)),
'hidden' => ! $visible,
match ($width) {
MaxWidth::ExtraSmall => 'max-w-xs',
MaxWidth::Small => 'max-w-sm',
MaxWidth::Medium => 'max-w-md',
MaxWidth::Large => 'max-w-lg',
MaxWidth::ExtraLarge => 'max-w-xl',
MaxWidth::TwoExtraLarge => 'max-w-2xl',
MaxWidth::ThreeExtraLarge => 'max-w-3xl',
MaxWidth::FourExtraLarge => 'max-w-4xl',
MaxWidth::FiveExtraLarge => 'max-w-5xl',
MaxWidth::SixExtraLarge => 'max-w-6xl',
MaxWidth::SevenExtraLarge => 'max-w-7xl',
MaxWidth::Full => 'max-w-full',
MaxWidth::MinContent => 'max-w-min',
MaxWidth::MaxContent => 'max-w-max',
MaxWidth::FitContent => 'max-w-fit',
MaxWidth::Prose => 'max-w-prose',
MaxWidth::ScreenSmall => 'max-w-screen-sm',
MaxWidth::ScreenMedium => 'max-w-screen-md',
MaxWidth::ScreenLarge => 'max-w-screen-lg',
MaxWidth::ScreenExtraLarge => 'max-w-screen-xl',
MaxWidth::ScreenTwoExtraLarge => 'max-w-screen-2xl',
MaxWidth::Screen => 'fixed inset-0',
default => $width,
},
])
}}
>
@if ($heading || $header)
<div
@class([
'fi-modal-header flex px-6 pt-6',
'pb-6' => (! $hasSlot) && (! $hasFooter),
'fi-sticky sticky top-0 z-10 border-b border-gray-200 bg-white pb-6 dark:border-white/10 dark:bg-gray-900' => $stickyHeader,
'rounded-t-xl' => $stickyHeader && ! ($slideOver || ($width === MaxWidth::Screen)),
match ($alignment) {
Alignment::Start, Alignment::Left => 'gap-x-5',
Alignment::Center => 'flex-col',
default => null,
},
'items-center' => $hasIcon && $hasHeading && (! $hasDescription) && in_array($alignment, [Alignment::Start, Alignment::Left]),
])
>
@if ($closeButton)
<div
@class([
'absolute',
'end-4 top-4' => ! $slideOver,
'end-6 top-6' => $slideOver,
])
>
<x-filament::icon-button
color="gray"
icon="heroicon-o-x-mark"
icon-alias="modal.close-button"
icon-size="lg"
:label="__('filament::components/modal.actions.close.label')"
tabindex="-1"
:x-on:click="$closeEventHandler"
class="fi-modal-close-btn"
/>
</div>
@endif
@if ($header)
{{ $header }}
@else
@if ($hasIcon)
<div
@class([
'mb-5 flex items-center justify-center' => $alignment === Alignment::Center,
])
>
<div
@class([
'rounded-full',
match ($iconColor) {
'gray' => 'bg-gray-100 dark:bg-gray-500/20',
default => 'fi-color-custom bg-custom-100 dark:bg-custom-500/20',
},
is_string($iconColor) ? "fi-color-{$iconColor}" : null,
match ($alignment) {
Alignment::Start, Alignment::Left => 'p-2',
Alignment::Center => 'p-3',
default => null,
},
])
@style([
\Filament\Support\get_color_css_variables(
$iconColor,
shades: [100, 400, 500, 600],
alias: 'modal.icon',
) => $iconColor !== 'gray',
])
>
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
@class([
'fi-modal-icon h-6 w-6',
match ($iconColor) {
'gray' => 'text-gray-500 dark:text-gray-400',
default => 'text-custom-600 dark:text-custom-400',
},
])
/>
</div>
</div>
@endif
<div
@class([
'text-center' => $alignment === Alignment::Center,
])
>
<x-filament::modal.heading
@class([
'me-6' => $closeButton && ((! $hasIcon) || in_array($alignment, [Alignment::Start, Alignment::Left])),
'ms-6' => $closeButton && (! $hasIcon) && ($alignment === Alignment::Center),
])
>
{{ $heading }}
</x-filament::modal.heading>
@if ($hasDescription)
<x-filament::modal.description
class="mt-2"
>
{{ $description }}
</x-filament::modal.description>
@endif
</div>
@endif
</div>
@endif
@if ($hasSlot)
<div
@class([
'fi-modal-content flex flex-col gap-y-4 py-6',
'flex-1' => ($width === MaxWidth::Screen) || $slideOver,
'pe-6 ps-[5.25rem]' => $hasIcon && ($alignment === Alignment::Start) && (! $stickyHeader),
'px-6' => ! ($hasIcon && ($alignment === Alignment::Start) && (! $stickyHeader)),
])
>
{{ $slot }}
</div>
@endif
@if ($hasFooter)
<div
@class([
'fi-modal-footer w-full',
'pe-6 ps-[5.25rem]' => $hasIcon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter),
'px-6' => ! ($hasIcon && ($alignment === Alignment::Start) && ($footerActionsAlignment !== Alignment::Center) && (! $stickyFooter)),
'fi-sticky sticky bottom-0 border-t border-gray-200 bg-white py-5 dark:border-white/10 dark:bg-gray-900' => $stickyFooter,
'rounded-b-xl' => $stickyFooter && ! ($slideOver || ($width === MaxWidth::Screen)),
'pb-6' => ! $stickyFooter,
'mt-6' => (! $stickyFooter) && (! $hasSlot),
'mt-auto' => $slideOver,
])
>
@if (! \Filament\Support\is_slot_empty($footer))
{{ $footer }}
@else
<div
@class([
'fi-modal-footer-actions gap-3',
match ($footerActionsAlignment) {
Alignment::Start, Alignment::Left => 'flex flex-wrap items-center',
Alignment::Center => 'flex flex-col-reverse sm:grid sm:grid-cols-[repeat(auto-fit,minmax(0,1fr))]',
Alignment::End, Alignment::Right => 'flex flex-row-reverse flex-wrap items-center',
default => null,
},
])
>
@if (is_array($footerActions))
@foreach ($footerActions as $action)
{{ $action }}
@endforeach
@else
{{ $footerActions }}
@endif
</div>
@endif
</div>
@endif
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,190 @@
@props([
'currentPageOptionProperty' => 'tableRecordsPerPage',
'extremeLinks' => false,
'paginator',
'pageOptions' => [],
])
@php
use Illuminate\Contracts\Pagination\CursorPaginator;
$isRtl = __('filament-panels::layout.direction') === 'rtl';
$isSimple = ! $paginator instanceof \Illuminate\Pagination\LengthAwarePaginator;
@endphp
<nav
aria-label="{{ __('filament::components/pagination.label') }}"
role="navigation"
{{
$attributes->class([
'fi-pagination grid grid-cols-[1fr_auto_1fr] items-center gap-x-3',
'fi-simple' => $isSimple,
])
}}
>
@if (! $paginator->onFirstPage())
@php
if ($paginator instanceof CursorPaginator) {
$wireClickAction = "setPage('{$paginator->previousCursor()->encode()}', '{$paginator->getCursorName()}')";
} else {
$wireClickAction = "previousPage('{$paginator->getPageName()}')";
}
@endphp
<x-filament::button
color="gray"
rel="prev"
:wire:click="$wireClickAction"
:wire:key="$this->getId() . '.pagination.previous'"
class="fi-pagination-previous-btn justify-self-start"
>
{{ __('filament::components/pagination.actions.previous.label') }}
</x-filament::button>
@endif
@if (! $isSimple)
<span
class="fi-pagination-overview text-sm font-medium text-gray-700 dark:text-gray-200"
>
{{
trans_choice(
'filament::components/pagination.overview',
$paginator->total(),
[
'first' => \Illuminate\Support\Number::format($paginator->firstItem() ?? 0),
'last' => \Illuminate\Support\Number::format($paginator->lastItem() ?? 0),
'total' => \Illuminate\Support\Number::format($paginator->total()),
],
)
}}
</span>
@endif
@if (count($pageOptions) > 1)
<div class="col-start-2 justify-self-center">
<label class="fi-pagination-records-per-page-select fi-compact">
<x-filament::input.wrapper>
<x-filament::input.select
:wire:model.live="$currentPageOptionProperty"
>
@foreach ($pageOptions as $option)
<option value="{{ $option }}">
{{ $option === 'all' ? __('filament::components/pagination.fields.records_per_page.options.all') : $option }}
</option>
@endforeach
</x-filament::input.select>
</x-filament::input.wrapper>
<span class="sr-only">
{{ __('filament::components/pagination.fields.records_per_page.label') }}
</span>
</label>
<label class="fi-pagination-records-per-page-select">
<x-filament::input.wrapper
:prefix="__('filament::components/pagination.fields.records_per_page.label')"
>
<x-filament::input.select
:wire:model.live="$currentPageOptionProperty"
>
@foreach ($pageOptions as $option)
<option value="{{ $option }}">
{{ $option === 'all' ? __('filament::components/pagination.fields.records_per_page.options.all') : $option }}
</option>
@endforeach
</x-filament::input.select>
</x-filament::input.wrapper>
</label>
</div>
@endif
@if ($paginator->hasMorePages())
@php
if ($paginator instanceof CursorPaginator) {
$wireClickAction = "setPage('{$paginator->nextCursor()->encode()}', '{$paginator->getCursorName()}')";
} else {
$wireClickAction = "nextPage('{$paginator->getPageName()}')";
}
@endphp
<x-filament::button
color="gray"
rel="next"
:wire:click="$wireClickAction"
:wire:key="$this->getId() . '.pagination.next'"
class="fi-pagination-next-btn col-start-3 justify-self-end"
>
{{ __('filament::components/pagination.actions.next.label') }}
</x-filament::button>
@endif
@if ((! $isSimple) && $paginator->hasPages())
<ol
class="fi-pagination-items justify-self-end rounded-lg bg-white shadow-sm ring-1 ring-gray-950/10 dark:bg-white/5 dark:ring-white/20"
>
@if (! $paginator->onFirstPage())
@if ($extremeLinks)
<x-filament::pagination.item
:aria-label="__('filament::components/pagination.actions.first.label')"
:icon="$isRtl ? 'heroicon-m-chevron-double-right' : 'heroicon-m-chevron-double-left'"
:icon-alias="$isRtl ? 'pagination.first-button.rtl' : 'pagination.first-button'"
rel="first"
:wire:click="'gotoPage(1, \'' . $paginator->getPageName() . '\')'"
:wire:key="$this->getId() . '.pagination.first'"
/>
@endif
<x-filament::pagination.item
:aria-label="__('filament::components/pagination.actions.previous.label')"
:icon="$isRtl ? 'heroicon-m-chevron-right' : 'heroicon-m-chevron-left'"
{{-- @deprecated Use `pagination.previous-button.rtl` instead of `pagination.previous-button` for RTL. --}}
:icon-alias="$isRtl ? ['pagination.previous-button.rtl', 'pagination.previous-button'] : 'pagination.previous-button'"
rel="prev"
:wire:click="'previousPage(\'' . $paginator->getPageName() . '\')'"
:wire:key="$this->getId() . '.pagination.previous'"
/>
@endif
@foreach ($paginator->render()->offsetGet('elements') as $element)
@if (is_string($element))
<x-filament::pagination.item disabled :label="$element" />
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
<x-filament::pagination.item
:active="$page === $paginator->currentPage()"
:aria-label="trans_choice('filament::components/pagination.actions.go_to_page.label', $page, ['page' => $page])"
:label="$page"
:wire:click="'gotoPage(' . $page . ', \'' . $paginator->getPageName() . '\')'"
:wire:key="$this->getId() . '.pagination.' . $paginator->getPageName() . '.' . $page"
/>
@endforeach
@endif
@endforeach
@if ($paginator->hasMorePages())
<x-filament::pagination.item
:aria-label="__('filament::components/pagination.actions.next.label')"
:icon="$isRtl ? 'heroicon-m-chevron-left' : 'heroicon-m-chevron-right'"
{{-- @deprecated Use `pagination.next-button.rtl` instead of `pagination.next-button` for RTL. --}}
:icon-alias="$isRtl ? ['pagination.next-button.rtl', 'pagination.next-button'] : 'pagination.next-button'"
rel="next"
:wire:click="'nextPage(\'' . $paginator->getPageName() . '\')'"
:wire:key="$this->getId() . '.pagination.next'"
/>
@if ($extremeLinks)
<x-filament::pagination.item
:aria-label="__('filament::components/pagination.actions.last.label')"
:icon="$isRtl ? 'heroicon-m-chevron-double-left' : 'heroicon-m-chevron-double-right'"
:icon-alias="$isRtl ? 'pagination.last-button.rtl' : 'pagination.last-button'"
rel="last"
:wire:click="'gotoPage(' . $paginator->lastPage() . ', \'' . $paginator->getPageName() . '\')'"
:wire:key="$this->getId() . '.pagination.last'"
/>
@endif
@endif
</ol>
@endif
</nav>

View File

@ -0,0 +1,50 @@
@props([
'active' => false,
'ariaLabel' => null,
'disabled' => false,
'icon' => null,
'iconAlias' => null,
'label' => null,
])
<li
{{
$attributes->class([
'fi-pagination-item group/item border-x-[0.5px] border-gray-200 first:border-s-0 last:border-e-0 dark:border-white/10',
'fi-disabled' => $disabled,
'fi-active' => $active,
])
}}
>
<button
aria-label="{{ $ariaLabel }}"
@disabled($disabled)
type="button"
@class([
'fi-pagination-item-button group/button relative flex overflow-hidden p-2 outline-none transition duration-75 group-first/item:rounded-s-lg group-last/item:rounded-e-lg',
'hover:bg-gray-50 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-primary-600 dark:hover:bg-white/5 dark:focus-visible:ring-primary-500' => ! $disabled,
'bg-gray-50 dark:bg-white/5' => $active,
])
>
@if (filled($icon))
<x-filament::icon
:alias="$iconAlias"
:icon="$icon"
class="fi-pagination-item-icon h-5 w-5 text-gray-400 transition duration-75 group-hover/button:text-gray-500 dark:text-gray-500 dark:group-hover/button:text-gray-400"
/>
@endif
@if (filled($label))
<span
@class([
'fi-pagination-item-label px-1.5 text-sm font-semibold',
'text-gray-700 dark:text-gray-200' => ! ($disabled || $active),
'text-gray-500 dark:text-gray-400' => $disabled,
'text-primary-600 dark:text-primary-400' => $active,
])
>
{{ $label ?? '...' }}
</span>
@endif
</button>
</li>

View File

@ -0,0 +1,5 @@
<p
{{ $attributes->class(['fi-section-header-description overflow-hidden break-words text-sm text-gray-500 dark:text-gray-400']) }}
>
{{ $slot }}
</p>

View File

@ -0,0 +1,5 @@
<h3
{{ $attributes->class(['fi-section-header-heading text-base font-semibold leading-6 text-gray-950 dark:text-white']) }}
>
{{ $slot }}
</h3>

View File

@ -0,0 +1,213 @@
@php
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
@endphp
@props([
'aside' => false,
'collapsed' => false,
'collapsible' => false,
'compact' => false,
'contentBefore' => false,
'description' => null,
'footerActions' => [],
'footerActionsAlignment' => Alignment::Start,
'headerActions' => [],
'headerEnd' => null,
'heading' => null,
'icon' => null,
'iconColor' => 'gray',
'iconSize' => IconSize::Large,
'persistCollapsed' => false,
])
@php
$hasDescription = filled((string) $description);
$hasHeading = filled($heading);
$hasIcon = filled($icon);
if (is_array($headerActions)) {
$headerActions = array_filter(
$headerActions,
fn ($headerAction): bool => $headerAction->isVisible(),
);
}
if (is_array($footerActions)) {
$footerActions = array_filter(
$footerActions,
fn ($footerAction): bool => $footerAction->isVisible(),
);
}
$hasHeaderActions = $headerActions instanceof \Illuminate\Contracts\Support\Htmlable
? ! \Filament\Support\is_slot_empty($headerActions)
: filled($headerActions);
$hasFooterActions = $footerActions instanceof \Illuminate\Contracts\Support\Htmlable
? ! \Filament\Support\is_slot_empty($footerActions)
: filled($footerActions);
$hasHeader = $hasIcon || $hasHeading || $hasDescription || $collapsible || $hasHeaderActions || filled((string) $headerEnd);
@endphp
<section
{{-- TODO: Investigate Livewire bug - https://github.com/filamentphp/filament/pull/8511 --}}
x-data="{
isCollapsed: @if ($persistCollapsed) $persist(@js($collapsed)).as(`section-${$el.id}-isCollapsed`) @else @js($collapsed) @endif,
}"
@if ($collapsible)
x-on:collapse-section.window="if ($event.detail.id == $el.id) isCollapsed = true"
x-on:expand="isCollapsed = false"
x-on:open-section.window="if ($event.detail.id == $el.id) isCollapsed = false"
x-on:toggle-section.window="if ($event.detail.id == $el.id) isCollapsed = ! isCollapsed"
x-bind:class="isCollapsed && 'fi-collapsed'"
@endif
{{
$attributes->class([
'fi-section',
match ($aside) {
true => 'fi-aside grid grid-cols-1 items-start gap-x-6 gap-y-4 md:grid-cols-3',
false => 'rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10',
},
])
}}
>
@if ($hasHeader)
<header
@if ($collapsible)
x-on:click="isCollapsed = ! isCollapsed"
@endif
@class([
'fi-section-header flex flex-col gap-3',
'cursor-pointer' => $collapsible,
match ($compact) {
true => 'px-4 py-2.5',
false => 'px-6 py-4',
} => ! $aside,
])
>
<div class="flex items-center gap-3">
@if ($hasIcon)
<x-filament::icon
:icon="$icon"
@class([
'fi-section-header-icon self-start',
match ($iconColor) {
'gray' => 'text-gray-400 dark:text-gray-500',
default => 'fi-color-custom text-custom-500 dark:text-custom-400',
},
is_string($iconColor) ? "fi-color-{$iconColor}" : null,
match ($iconSize) {
IconSize::Small, 'sm' => 'h-4 w-4 mt-1',
IconSize::Medium, 'md' => 'h-5 w-5 mt-0.5',
IconSize::Large, 'lg' => 'h-6 w-6',
default => $iconSize,
},
])
@style([
\Filament\Support\get_color_css_variables(
$iconColor,
shades: [400, 500],
alias: 'section.header.icon',
) => $iconColor !== 'gray',
])
/>
@endif
@if ($hasHeading || $hasDescription)
<div class="grid flex-1 gap-y-1">
@if ($hasHeading)
<x-filament::section.heading>
{{ $heading }}
</x-filament::section.heading>
@endif
@if ($hasDescription)
<x-filament::section.description>
{{ $description }}
</x-filament::section.description>
@endif
</div>
@endif
@if ($hasHeaderActions)
<div class="hidden sm:block">
<x-filament::actions
:actions="$headerActions"
:alignment="\Filament\Support\Enums\Alignment::Start"
x-on:click.stop=""
/>
</div>
@endif
{{ $headerEnd }}
@if ($collapsible)
<x-filament::icon-button
color="gray"
icon="heroicon-m-chevron-down"
icon-alias="section.collapse-button"
x-on:click.stop="isCollapsed = ! isCollapsed"
x-bind:class="{ 'rotate-180': ! isCollapsed }"
/>
@endif
</div>
@if ($hasHeaderActions)
<div class="sm:hidden">
<x-filament::actions
:actions="$headerActions"
:alignment="\Filament\Support\Enums\Alignment::Start"
x-on:click.stop=""
/>
</div>
@endif
</header>
@endif
<div
@if ($collapsible)
x-bind:aria-expanded="(! isCollapsed).toString()"
@if ($collapsed || $persistCollapsed)
x-cloak
@endif
x-bind:class="{
'invisible absolute h-0 overflow-hidden border-none': isCollapsed,
}"
@endif
@class([
'fi-section-content-ctn',
'border-t border-gray-200 dark:border-white/10' => $hasHeader && (! $aside),
'rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10 md:col-span-2' => $aside,
'md:order-first' => $contentBefore,
])
>
<div
@class([
'fi-section-content',
match ($compact) {
true => 'p-4',
false => 'p-6',
},
])
>
{{ $slot }}
</div>
@if ($hasFooterActions)
<footer
@class([
'fi-section-footer border-t border-gray-200 dark:border-white/10',
'px-6 py-4' => ! $compact,
'px-4 py-2.5' => $compact,
])
>
<x-filament::actions
:actions="$footerActions"
:alignment="$footerActionsAlignment"
/>
</footer>
@endif
</div>
</section>

View File

@ -0,0 +1,21 @@
@props([
'contained' => false,
'label' => null,
])
<nav
{{
$attributes
->merge([
'aria-label' => $label,
'role' => 'tablist',
])
->class([
'fi-tabs flex max-w-full gap-x-1 overflow-x-auto',
'fi-contained border-b border-gray-200 px-3 py-2.5 dark:border-white/10' => $contained,
'mx-auto rounded-xl bg-white p-2 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10' => ! $contained,
])
}}
>
{{ $slot }}
</nav>

View File

@ -0,0 +1,123 @@
@php
use Filament\Support\Enums\IconPosition;
@endphp
@props([
'active' => false,
'alpineActive' => null,
'badge' => null,
'badgeColor' => null,
'badgeTooltip' => null,
'badgeIcon' => null,
'badgeIconPosition' => IconPosition::Before,
'href' => null,
'icon' => null,
'iconColor' => 'gray',
'iconPosition' => IconPosition::Before,
'spaMode' => null,
'tag' => 'button',
'target' => null,
'type' => 'button',
])
@php
if (! $iconPosition instanceof IconPosition) {
$iconPosition = filled($iconPosition) ? (IconPosition::tryFrom($iconPosition) ?? $iconPosition) : null;
}
$hasAlpineActiveClasses = filled($alpineActive);
$inactiveItemClasses = 'hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5';
// @deprecated `fi-tabs-item-active` has been replaced by `fi-active`.
$activeItemClasses = 'fi-active fi-tabs-item-active bg-gray-50 dark:bg-white/5';
$inactiveLabelClasses = 'text-gray-500 group-hover:text-gray-700 group-focus-visible:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200 dark:group-focus-visible:text-gray-200';
$activeLabelClasses = 'text-primary-600 dark:text-primary-400';
$iconClasses = 'fi-tabs-item-icon h-5 w-5 shrink-0 transition duration-75';
$inactiveIconClasses = 'text-gray-400 dark:text-gray-500';
$activeIconClasses = 'text-primary-600 dark:text-primary-400';
@endphp
<{{ $tag }}
@if ($tag === 'button')
type="{{ $type }}"
@elseif ($tag === 'a')
{{ \Filament\Support\generate_href_html($href, $target === '_blank', $spaMode) }}
@endif
@if ($hasAlpineActiveClasses)
x-bind:class="{
@js($inactiveItemClasses): {{-- format-ignore-start --}} ! ({{ $alpineActive }}) {{-- format-ignore-end --}},
@js($activeItemClasses): {{ $alpineActive }},
}"
@endif
{{
$attributes
->merge([
'aria-selected' => $active,
'role' => 'tab',
])
->class([
'fi-tabs-item group flex items-center justify-center gap-x-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm font-medium outline-none transition duration-75',
$inactiveItemClasses => (! $hasAlpineActiveClasses) && (! $active),
$activeItemClasses => (! $hasAlpineActiveClasses) && $active,
])
}}
>
@if ($icon && $iconPosition === IconPosition::Before)
<x-filament::icon
:icon="$icon"
:x-bind:class="$hasAlpineActiveClasses ? '{ ' . \Illuminate\Support\Js::from($inactiveIconClasses) . ': ! (' . $alpineActive . '), ' . \Illuminate\Support\Js::from($activeIconClasses) . ': ' . $alpineActive . ' }' : null"
@class([
$iconClasses,
$inactiveIconClasses => (! $hasAlpineActiveClasses) && (! $active),
$activeIconClasses => (! $hasAlpineActiveClasses) && $active,
])
/>
@endif
<span
@if ($hasAlpineActiveClasses)
x-bind:class="{
@js($inactiveLabelClasses): {{-- format-ignore-start --}} ! ({{ $alpineActive }}) {{-- format-ignore-end --}},
@js($activeLabelClasses): {{ $alpineActive }},
}"
@endif
@class([
'fi-tabs-item-label transition duration-75',
$inactiveLabelClasses => (! $hasAlpineActiveClasses) && (! $active),
$activeLabelClasses => (! $hasAlpineActiveClasses) && $active,
])
>
{{ $slot }}
</span>
@if ($icon && $iconPosition === IconPosition::After)
<x-filament::icon
:icon="$icon"
:x-bind:class="$hasAlpineActiveClasses ? '{ ' . \Illuminate\Support\Js::from($inactiveIconClasses) . ': ! (' . $alpineActive . '), ' . \Illuminate\Support\Js::from($activeIconClasses) . ': ' . $alpineActive . ' }' : null"
@class([
$iconClasses,
$inactiveIconClasses => (! $hasAlpineActiveClasses) && (! $active),
$activeIconClasses => (! $hasAlpineActiveClasses) && $active,
])
/>
@endif
@if (filled($badge))
<x-filament::badge
:color="$badgeColor"
:icon="$badgeIcon"
:icon-position="$badgeIconPosition"
size="sm"
:tooltip="$badgeTooltip"
class="w-max"
>
{{ $badge }}
</x-filament::badge>
@endif
</{{ $tag }}>