This commit is contained in:
Dan Harrin
2025-05-23 17:35:33 +01:00
parent dbf348e50e
commit 6eff44a749
12 changed files with 624 additions and 7 deletions
@@ -28,6 +28,7 @@ use Filament\Forms\Components\TimePicker;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\FusedGroup;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Icon;
use Filament\Schemas\Components\Section;
@@ -62,6 +63,35 @@ class FieldsDemo extends Component implements HasActions, HasSchemas
return $form
->statePath('data')
->components([
Group::make()
->id('fused')
->extraAttributes([
'class' => 'p-16 max-w-xl',
])
->schema([
FusedGroup::make([
TextInput::make('one')
->placeholder('One')
->suffix('hey'),
Select::make('two')
->placeholder('Two'),
])->label('Test'),
]),
Group::make()
->id('fused2')
->extraAttributes([
'class' => 'p-16 max-w-xl',
])
->schema([
FusedGroup::make([
TextInput::make('one')
->placeholder('One'),
Select::make('two')
->placeholder('Two'),
Select::make('three')
->placeholder('Three'),
])->columns(3),
]),
Group::make()
->id('simple')
->extraAttributes([
@@ -29,8 +29,8 @@
$statePath ??= $field->getStatePath();
}
$aboveLabelSchema = $field?->getChildSchema($field::ABOVE_LABEL_SCHEMA_KEY);
$belowLabelSchema = $field?->getChildSchema($field::BELOW_LABEL_SCHEMA_KEY);
$aboveLabelSchema = $field?->getChildSchema($field::ABOVE_LABEL_SCHEMA_KEY)?->toHtmlString();
$belowLabelSchema = $field?->getChildSchema($field::BELOW_LABEL_SCHEMA_KEY)?->toHtmlString();
$beforeLabelSchema = $field?->getChildSchema($field::BEFORE_LABEL_SCHEMA_KEY)?->toHtmlString();
$afterLabelSchema = $field?->getChildSchema($field::AFTER_LABEL_SCHEMA_KEY)?->toHtmlString();
$aboveContentSchema = $field?->getChildSchema($field::ABOVE_CONTENT_SCHEMA_KEY)?->toHtmlString();
@@ -0,0 +1,33 @@
@props([
'field' => null,
'id' => null,
'label' => null,
])
@php
use Illuminate\View\ComponentAttributeBag;
if ($field) {
$id ??= $field->getId();
$label ??= $field->getLabel();
}
@endphp
<div
data-field-wrapper
{{
(new ComponentAttributeBag())
->merge($field?->getExtraFieldWrapperAttributes() ?? [], escape: false)
->class([
'fi-fo-field',
])
}}
>
@if (filled($label))
<label for="{{ $id }}" class="fi-fo-field-label fi-sr-only">
{{ $label }}
</label>
@endif
{{ $slot }}
</div>
@@ -494,7 +494,7 @@ class DateTimePicker extends Field implements HasAffixActions
public function getTimezone(): string
{
return $this->evaluate($this->timezone) ?? ($this->hasTime() ? FilamentTimezone::get() : app('app.timezone'));
return $this->evaluate($this->timezone) ?? ($this->hasTime() ? FilamentTimezone::get() : config('app.timezone'));
}
public function getLocale(): string
@@ -435,7 +435,7 @@ trait CanFormatState
public function getTimezone(): string
{
return $this->evaluate($this->timezone) ?? ($this->isDateTime() ? FilamentTimezone::get() : app('app.timezone'));
return $this->evaluate($this->timezone) ?? ($this->isDateTime() ? FilamentTimezone::get() : config('app.timezone'));
}
public function isHtml(): bool
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,281 @@
.fi-sc-fused-group {
@apply flex h-full flex-col gap-2;
& .fi-sc-fused-group-label-ctn {
@apply flex items-center gap-x-3;
& .fi-sc-fused-group-label {
@apply text-sm leading-6 font-medium text-gray-950 dark:text-white;
& .fi-sc-fused-group-label-required-mark {
@apply text-danger-600 dark:text-danger-400 font-medium;
}
}
}
& > .fi-sc {
@apply shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20 focus-within:ring-primary-600 dark:focus-within:ring-primary-500 focus-within:ring-2;
}
& .fi-sc {
@apply rounded-lg gap-px bg-gray-950/10 dark:bg-white/20;
& > :first-child .fi-input-wrp {
@apply rounded-t-lg;
}
& > :last-child .fi-input-wrp {
@apply rounded-b-lg;
}
&.sm\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply sm:rounded-se-none sm:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply sm:rounded-es-none sm:rounded-se-lg;
}
}
&.md\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply md:rounded-se-none md:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply md:rounded-es-none md:rounded-se-lg;
}
}
&.lg\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply lg:rounded-se-none lg:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply lg:rounded-es-none lg:rounded-se-lg;
}
}
&.xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply xl:rounded-se-none xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply xl:rounded-es-none xl:rounded-se-lg;
}
}
&.\32xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply 2xl:rounded-se-none 2xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply 2xl:rounded-es-none 2xl:rounded-se-lg;
}
}
@supports (container-type: inline-size) {
&.\@3xs\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @3xs:rounded-se-none @3xs:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @3xs:rounded-es-none @3xs:rounded-se-lg;
}
}
&.\@2xs\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @2xs:rounded-se-none @2xs:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @2xs:rounded-es-none @2xs:rounded-se-lg;
}
}
&.\@xs\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @xs:rounded-se-none @xs:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @xs:rounded-es-none @xs:rounded-se-lg;
}
}
&.\@sm\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @sm:rounded-se-none @sm:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @sm:rounded-es-none @sm:rounded-se-lg;
}
}
&.\@md\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @md:rounded-se-none @md:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @md:rounded-es-none @md:rounded-se-lg;
}
}
&.\@lg\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @lg:rounded-se-none @lg:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @lg:rounded-es-none @lg:rounded-se-lg;
}
}
&.\@xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @xl:rounded-se-none @xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @xl:rounded-es-none @xl:rounded-se-lg;
}
}
&.\@2xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @2xl:rounded-se-none @2xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @2xl:rounded-es-none @2xl:rounded-se-lg;
}
}
&.\@3xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @3xl:rounded-se-none @3xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @3xl:rounded-es-none @3xl:rounded-se-lg;
}
}
&.\@4xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @4xl:rounded-se-none @4xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @4xl:rounded-es-none @4xl:rounded-se-lg;
}
}
&.\@5xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @5xl:rounded-se-none @5xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @5xl:rounded-es-none @5xl:rounded-se-lg;
}
}
&.\@6xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @6xl:rounded-se-none @6xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @6xl:rounded-es-none @6xl:rounded-se-lg;
}
}
&.\@7xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply @7xl:rounded-se-none @7xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply @7xl:rounded-es-none @7xl:rounded-se-lg;
}
}
}
@supports not (container-type: inline-size) {
&.\!\@sm\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply sm:rounded-se-none sm:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply sm:rounded-es-none sm:rounded-se-lg;
}
}
&.\!\@md\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply md:rounded-se-none md:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply md:rounded-es-none md:rounded-se-lg;
}
}
&.\!\@lg\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply lg:rounded-se-none lg:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply lg:rounded-es-none lg:rounded-se-lg;
}
}
&.\!\@xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply xl:rounded-se-none xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply xl:rounded-es-none xl:rounded-se-lg;
}
}
&.\!\@2xl\:fi-grid-cols {
& > :first-child .fi-input-wrp {
@apply 2xl:rounded-se-none 2xl:rounded-es-lg;
}
& > :last-child .fi-input-wrp {
@apply 2xl:rounded-es-none 2xl:rounded-se-lg;
}
}
}
}
& .fi-input-wrp {
@apply rounded-none ring-0 shadow-none;
&:not(.fi-disabled):not(:has(.fi-ac-action:focus)) {
@apply focus-within:ring-0;
&.fi-invalid {
@apply focus-within:ring-0;
}
}
}
& .fi-sc-fused-group-error-message {
@apply text-danger-600 dark:text-danger-400 text-sm;
}
}
+1
View File
@@ -1,6 +1,7 @@
@import './components/actions.css' layer(components);
@import './components/flex.css' layer(components);
@import './components/form.css' layer(components);
@import './components/fused-group.css' layer(components);
@import './components/icon.css' layer(components);
@import './components/image.css' layer(components);
@import './components/section.css' layer(components);
@@ -0,0 +1,97 @@
@php
use Filament\Forms\Components\Contracts\HasNestedRecursiveValidationRules;
use Filament\Forms\Components\Field;
$errorMessage = null;
foreach ($getDefaultChildComponents() as $childComponent) {
if (! ($childComponent instanceof Field)) {
continue;
}
$statePath = $childComponent->getStatePath();
if (blank($statePath)) {
continue;
}
if ($errors->has($statePath)) {
$errorMessage = $errors->first($statePath);
$areHtmlValidationMessagesAllowed = $childComponent->areHtmlValidationMessagesAllowed();
break;
}
if (! ($childComponent instanceof HasNestedRecursiveValidationRules)) {
continue;
}
if ($errors->has("{$statePath}.*")) {
$errorMessage = $errors->first("{$statePath}.*");
$areHtmlValidationMessagesAllowed = $childComponent->areHtmlValidationMessagesAllowed();
break;
}
}
@endphp
<div
{{
$attributes
->merge([
'id' => $getId(),
], escape: false)
->merge($getExtraAttributes(), escape: false)
->class(['fi-sc-fused-group'])
}}
>
@if (filled($label = $getLabel()))
<div class="fi-sc-fused-group-label-ctn">
{{ $getChildSchema($schemaComponent::BEFORE_LABEL_SCHEMA_KEY) }}
<div class="fi-sc-fused-group-label">
{{ $label }}@if ($isMarkedAsRequired())
<sup class="fi-sc-fused-group-label-required-mark">*</sup>
@endif
</div>
{{ $getChildSchema($schemaComponent::AFTER_LABEL_SCHEMA_KEY) }}
</div>
@endif
@if ($aboveContentContainer = $getChildSchema($schemaComponent::ABOVE_CONTENT_SCHEMA_KEY)?->toHtmlString())
{{ $aboveContentContainer }}
@endif
{{ $getChildSchema() }}
@if ($belowContentContainer = $getChildSchema($schemaComponent::BELOW_CONTENT_SCHEMA_KEY)?->toHtmlString())
{{ $belowContentContainer }}
@endif
@if (filled($errorMessage))
@if ($aboveErrorMessageSchema = $getChildSchema($schemaComponent::ABOVE_ERROR_MESSAGE_SCHEMA_KEY)?->toHtmlString())
{{ $aboveErrorMessageSchema }}
@endif
@if ($areHtmlValidationMessagesAllowed)
<div
data-validation-error
class="fi-sc-fused-group-error-message"
>
{!! $errorMessage !!}
</div>
@else
<p
data-validation-error
class="fi-sc-fused-group-error-message"
>
{{ $errorMessage }}
</p>
@endif
@if ($belowErrorMessageSchema = $getChildSchema($schemaComponent::BELOW_ERROR_MESSAGE_SCHEMA_KEY)?->toHtmlString())
{{ $belowErrorMessageSchema }}
@endif
@endif
</div>
@@ -0,0 +1,175 @@
<?php
namespace Filament\Schemas\Components;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Concerns\CanBeMarkedAsRequired;
use Filament\Forms\Components\Field;
use Filament\Schemas\Components\Concerns\EntanglesStateWithSingularRelationship;
use Filament\Schemas\Components\Concerns\HasLabel;
use Filament\Schemas\Components\Contracts\CanEntangleWithSingularRelationships;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Size;
class FusedGroup extends Component implements CanEntangleWithSingularRelationships
{
use CanBeMarkedAsRequired;
use EntanglesStateWithSingularRelationship;
use HasLabel;
/**
* @var view-string
*/
protected string $view = 'filament-schemas::components.fused-group';
const BEFORE_LABEL_SCHEMA_KEY = 'before_label';
const AFTER_LABEL_SCHEMA_KEY = 'after_label';
const ABOVE_CONTENT_SCHEMA_KEY = 'above_content';
const BELOW_CONTENT_SCHEMA_KEY = 'below_content';
const ABOVE_ERROR_MESSAGE_SCHEMA_KEY = 'above_error_message';
const BELOW_ERROR_MESSAGE_SCHEMA_KEY = 'below_error_message';
/**
* @param array<Component | Action | ActionGroup | string> | Closure $schema
*/
final public function __construct(array | Closure $schema = [])
{
$this->schema($schema);
}
/**
* @param array<Component | Action | ActionGroup | string> | Closure $schema
*/
public static function make(array | Closure $schema = []): static
{
$static = app(static::class, ['schema' => $schema]);
$static->configure();
return $static;
}
protected function setUp(): void
{
parent::setUp();
$this->fieldWrapperView('filament-forms::plain-field-wrapper');
$this->gap(false);
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function beforeLabel(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::BEFORE_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function afterLabel(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::AFTER_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function aboveContent(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::ABOVE_CONTENT_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function belowContent(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::BELOW_CONTENT_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function aboveErrorMessage(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::ABOVE_ERROR_MESSAGE_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup> | Schema | Component | Action | ActionGroup | string | Closure | null $components
*/
public function belowErrorMessage(array | Schema | Component | Action | ActionGroup | string | Closure | null $components): static
{
$this->childComponents($components, static::BELOW_ERROR_MESSAGE_SCHEMA_KEY);
return $this;
}
protected function makeChildSchema(string $key): Schema
{
$schema = parent::makeChildSchema($key);
if ($key === static::AFTER_LABEL_SCHEMA_KEY) {
$schema->alignEnd();
}
return $schema;
}
protected function configureChildSchema(Schema $schema, string $key): Schema
{
$schema = parent::configureChildSchema($schema, $key);
if (in_array($key, [
static::BEFORE_LABEL_SCHEMA_KEY,
static::AFTER_LABEL_SCHEMA_KEY,
static::ABOVE_CONTENT_SCHEMA_KEY,
static::BELOW_CONTENT_SCHEMA_KEY,
static::ABOVE_ERROR_MESSAGE_SCHEMA_KEY,
static::BELOW_ERROR_MESSAGE_SCHEMA_KEY,
])) {
$schema
->inline()
->embeddedInParentComponent()
->configureActionsUsing(fn (Action $action) => $action
->defaultSize(Size::Small)
->defaultView(Action::LINK_VIEW))
->configureActionGroupsUsing(fn (ActionGroup $actionGroup) => $actionGroup->defaultSize(Size::Small));
}
return $schema;
}
public function isRequired(): bool
{
foreach ($this->getDefaultChildComponents() as $component) {
if (! ($component instanceof Field)) {
continue;
}
if ($component->isMarkedAsRequired()) {
return true;
}
}
return false;
}
}
+1 -1
View File
@@ -13,6 +13,6 @@ class TimezoneManager
public function get(): string
{
return $this->timezone ?? app('app.timezone');
return $this->timezone ?? config('app.timezone');
}
}
@@ -439,7 +439,7 @@ trait CanFormatState
public function getTimezone(): string
{
return $this->evaluate($this->timezone) ?? ($this->isDateTime() ? FilamentTimezone::get() : app('app.timezone'));
return $this->evaluate($this->timezone) ?? ($this->isDateTime() ? FilamentTimezone::get() : config('app.timezone'));
}
public function isHtml(): bool