🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,144 @@
<?php
namespace Livewire\Features\SupportTesting {
use Closure;
class Testable {
/**
* @deprecated Use `mountAction()` instead.
*/
public function mountInfolistAction(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `unmountAction()` instead.
*/
public function unmountInfolistAction(): static {}
/**
* @deprecated Use `fillForm()` instead.
*/
public function setInfolistActionData(array $data): static {}
/**
* @deprecated Use `assertSchemaStateSet()` instead.
*/
public function assertInfolistActionDataSet(array $data): static {}
/**
* @deprecated Use `callAction()` instead.
*/
public function callInfolistAction(string $component, string | array $name, array $data = [], array $arguments = [], string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `callMountedAction()` instead.
*/
public function callMountedInfolistAction(array $arguments = []): static {}
/**
* @deprecated Use `assertActionExists()` instead.
*/
public function assertInfolistActionExists(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDoesNotExist()` instead.
*/
public function assertInfolistActionDoesNotExist(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionVisible()` instead.
*/
public function assertInfolistActionVisible(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionHidden()` instead.
*/
public function assertInfolistActionHidden(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionEnabled()` instead.
*/
public function assertInfolistActionEnabled(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDisabled()` instead.
*/
public function assertInfolistActionDisabled(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionMounted()` instead.
*/
public function assertInfolistActionMounted(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionNotMounted()` instead.
*/
public function assertInfolistActionNotMounted(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionHalted()` instead.
*/
public function assertInfolistActionHalted(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertHasFormErrors()` instead.
*/
public function assertHasInfolistActionErrors(array $keys = []): static {}
/**
* @deprecated Use `assertHasNoFormErrors()` instead.
*/
public function assertHasNoInfolistActionErrors(array $keys = []): static {}
/**
* @deprecated Use `assertActionHasIcon()` instead.
*/
public function assertInfolistActionHasIcon(string $component, string | array $name, string $icon, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveIcon()` instead.
*/
public function assertInfolistActionDoesNotHaveIcon(string $component, string | array $name, string $icon, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionHasLabel()` instead.
*/
public function assertInfolistActionHasLabel(string $component, string | array $name, string $label, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveLabel()` instead.
*/
public function assertInfolistActionDoesNotHaveLabel(string $component, string | array $name, string $label, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionHasColor()` instead.
*/
public function assertInfolistActionHasColor(string $component, string | array $name, string | array $color, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveColor()` instead.
*/
public function assertInfolistActionDoesNotHaveColor(string $component, string | array $name, string | array $color, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionHasUrl()` instead.
*/
public function assertInfolistActionHasUrl(string $component, string | array $name, string $url, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveUrl()` instead.
*/
public function assertInfolistActionDoesNotHaveUrl(string $component, string | array $name, string $url, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionShouldOpenUrlInNewTab()` instead.
*/
public function assertInfolistActionShouldOpenUrlInNewTab(string $component, string | array $name, string $infolistName = 'infolist'): static {}
/**
* @deprecated Use `assertActionShouldNotOpenUrlInNewTab()` instead.
*/
public function assertInfolistActionShouldNotOpenUrlInNewTab(string $component, string | array $name, string $infolistName = 'infolist'): static {}
}
}
@@ -0,0 +1,33 @@
{
"name": "filament/infolists",
"description": "Easily add beautiful read-only infolists to any Livewire component.",
"license": "MIT",
"homepage": "https://github.com/filamentphp/filament",
"support": {
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"require": {
"php": "^8.2",
"filament/actions": "self.version",
"filament/schemas": "self.version",
"filament/support": "self.version"
},
"autoload": {
"psr-4": {
"Filament\\Infolists\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Filament\\Infolists\\InfolistsServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,941 @@
---
title: Overview
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
<AutoScreenshot name="infolists/overview" alt="Product infolist example" version="4.x" />
Filament's infolists package lets you display a read-only list of data for a specific entity. It's integrated into other Filament packages, such as inside [panel resources](../resources), [relation managers](../resources/managing-relationships), and [action modals](../actions). Understanding how to use the infolist builder will save you time when building custom Livewire applications or working with other Filament features.
This guide covers the fundamentals of building infolists with Filament. If you want to add an infolist to your own Livewire component, [start here](../components/infolist) before continuing. If you're adding an infolist to a [panel resource](../resources), or using another Filament package, you're ready to begin!
## Defining entries
Entry classes can be found in the `Filament\Infolists\Components` namespace. They reside within the schema array of components. Filament includes a number of entries built-in:
- [Text entry](text-entry)
- [Icon entry](icon-entry)
- [Image entry](image-entry)
- [Color entry](color-entry)
- [Code entry](code-entry)
- [Key-value entry](key-value-entry)
- [Repeatable entry](repeatable-entry)
You may also [create your own custom entries](custom-entries) to display data however you wish.
Entries may be created using the static `make()` method, passing its unique name. Usually, the name of an entry corresponds to the name of an attribute on an Eloquent model. You may use "dot notation" to access attributes within relationships:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
TextEntry::make('author.name')
```
<AutoScreenshot name="infolists/entries/simple" alt="Entries in an infolist" version="4.x" />
## Entry content (state)
Entries may feel a bit magic at first, but they are designed to be simple to use and optimized to display data from an Eloquent record. Despite this, they are flexible and you can display data from any source, not just an Eloquent record attribute.
The data that an entry displays is called its "state". When using a [panel resource](../resources), the infolist is aware of the record it is displaying. This means that the state of the entry is set based on the value of the attribute on the record. For example, if the entry is used in the infolist of a `PostResource`, then the `title` attribute value of the current post will be displayed.
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
```
If you want to access the value stored in a relationship, you can use "dot notation". The name of the relationship that you would like to access data from comes first, followed by a dot, and then the name of the attribute:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('author.name')
```
You can also use "dot notation" to access values within a JSON / array column on an Eloquent model. The name of the attribute comes first, followed by a dot, and then the key of the JSON object you want to read from:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('meta.title')
```
### Setting the state of an entry
You can pass your own state to an entry by using the `state()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->state('Hello, world!')
```
<UtilityInjection set="infolistEntries" except="$state" version="4.x">The `state()` method also accepts a function to dynamically calculate the state. You can inject various utilities into the function as parameters.</UtilityInjection>
### Setting the default state of an entry
When an entry is empty (its state is `null`), you can use the `default()` method to define alternative state to use instead. This method will treat the default state as if it were real, so entries like [image](image-entry) or [color](color-entry) will display the default image or color.
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->default('Untitled')
```
#### Adding placeholder text if an entry is empty
Sometimes you may want to display placeholder text for entries with an empty state, which is styled as a lighter gray text. This differs from the [default value](#setting-the-default-state-of-an-entry), as the placeholder is always text and not treated as if it were real state.
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->placeholder('Untitled')
```
<AutoScreenshot name="infolists/entries/placeholder" alt="Entry with a placeholder for empty state" version="4.x" />
## Setting an entry's label
By default, the label of the entry, which is displayed in the header of the infolist, is generated from the name of the entry. You may customize this using the `label()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->label('Full name')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `label()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
Customizing the label in this way is useful if you wish to use a [translation string for localization](https://laravel.com/docs/localization#retrieving-translation-strings):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->label(__('entries.name'))
```
### Hiding an entry's label
<Aside variant="tip">
If you're looking to hide an entry's label, it might be the case that you are trying to use an entry for arbitrary text or UI. Entries are specifically designed to display data in a structured way, but [Prime components](../schemas/primes) are simple components that are used to render basic stand-alone static content, such as text, images, and buttons (actions). You may want to consider using a Prime component instead.
</Aside>
It may be tempting to set the label to an empty string to hide it, but this is not recommended. Setting the label to an empty string will not communicate the purpose of the entry to screen readers, even if the purpose is clear visually. Instead, you should use the `hiddenLabel()` method, so it is hidden visually but still accessible to screen readers:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->hiddenLabel()
```
Optionally, you may pass a boolean value to control if the label should be hidden or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->hiddenLabel(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `hiddenLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Opening a URL when an entry is clicked
When an entry is clicked, you may open a URL. To do this, pass a URL to the `url()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->url('/about/titles')
```
You may pass a function to the `url()` method to dynamically calculate the URL. For example, you may want to access the current Eloquent record for the infolist by injecting `$record` as an argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->url(fn (Post $record): string => route('posts.edit', ['post' => $record]))
```
If you're using a [panel resource](../resources), you can generate a link to a page for the record using the `getUrl()` method:
```php
use App\Filament\Posts\PostResource;
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->url(fn (Post $record): string => PostResource::getUrl('edit', ['record' => $record]))
```
<UtilityInjection set="infolistEntries" version="4.x">The function passed to `url()` can inject various utilities as parameters.</UtilityInjection>
You may also choose to open the URL in a new tab:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->url(fn (Post $record): string => PostResource::getUrl('edit', ['record' => $record]))
->openUrlInNewTab()
```
Optionally, you may pass a boolean value to control if the URL should open in a new tab or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->url(fn (Post $record): string => PostResource::getUrl('edit', ['record' => $record]))
->openUrlInNewTab(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `openUrlInNewTab()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Hiding an entry
You may hide an entry:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('role')
->hidden()
```
Optionally, you may pass a boolean value to control if the entry should be hidden or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('role')
->hidden(! FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `hidden()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
Alternatively, you may use the `visible()` method to control if the entry should be hidden or not. In some situations, this may help to make your code more readable:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('role')
->visible(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `visible()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="info">
If both `hidden()` and `visible()` are used, they both need to indicate that the entry should be visible for it to be shown.
</Aside>
### Hiding an entry using JavaScript
If you need to hide an entry based on a user interaction, you can use the `hidden()` or `visible()` methods, passing a function that uses utilities injected to determine whether the entry should be hidden or not:
```php
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\IconEntry;
Select::make('role')
->options([
'user' => 'User',
'staff' => 'Staff',
])
->live()
IconEntry::make('is_admin')
->boolean()
->hidden(fn (Get $get): bool => $get('role') !== 'staff')
```
In this example, the `role` field is set to `live()`, which means that the schema will reload the schema each time the `role` field is changed. This will cause the function that is passed to the `hidden()` method to be re-evaluated, which will hide the `is_admin` entry if the `role` field is not set to `staff`.
However, reloading the schema each time an entry causes a network request to be made, since there is no way to re-run the PHP function from the client-side. This is not ideal for performance.
Alternatively, you can write JavaScript to hide the entry based on the value of a field. This is done by passing a JavaScript expression to the `hiddenJs()` method:
```php
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\IconEntry;
Select::make('role')
->options([
'user' => 'User',
'staff' => 'Staff',
])
IconEntry::make('is_admin')
->boolean()
->hiddenJs(<<<'JS'
$get('role') !== 'staff'
JS)
```
Although the code passed to `hiddenJs()` looks very similar to PHP, it is actually JavaScript. Filament provides the `$get()` utility function to JavaScript that behaves very similar to its PHP equivalent, but without requiring the depended-on entry to be `live()`.
The `visibleJs()` method is also available, which works in the same way as `hiddenJs()`, but controls if the entry should be visible or not:
```php
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\IconEntry;
Select::make('role')
->options([
'user' => 'User',
'staff' => 'Staff',
])
IconEntry::make('is_admin')
->boolean()
->visibleJs(<<<'JS'
$get('role') === 'staff'
JS)
```
<Aside variant="info">
If both `hiddenJs()` and `visibleJs()` are used, they both need to indicate that the entry should be visible for it to be shown.
</Aside>
### Hiding an entry based on the current operation
The "operation" of a schema is the current action being performed on it. Usually, this is either `create`, `edit` or `view`, if you are using the [panel resource](../resources).
You can hide an entry based on the current operation by passing an operation to the `hiddenOn()` method:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_admin')
->boolean()
->hiddenOn('edit')
// is the same as
IconEntry::make('is_admin')
->boolean()
->hidden(fn (string $operation): bool => $operation === 'edit')
```
You can also pass an array of operations to the `hiddenOn()` method, and the entry will be hidden if the current operation is any of the operations in the array:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_admin')
->boolean()
->hiddenOn(['edit', 'view'])
// is the same as
IconEntry::make('is_admin')
->boolean()
->hidden(fn (string $operation): bool => in_array($operation, ['edit', 'view']))
```
<Aside variant="warning">
The `hiddenOn()` method will overwrite any previous calls to the `hidden()` method, and vice versa.
</Aside>
Alternatively, you may use the `visibleOn()` method to control if the entry should be hidden or not. In some situations, this may help to make your code more readable:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_admin')
->boolean()
->visibleOn('create')
IconEntry::make('is_admin')
->boolean()
->visibleOn(['create', 'edit'])
```
<Aside variant="info">
The `visibleOn()` method will overwrite any previous calls to the `visible()` method, and vice versa.
</Aside>
## Inline labels
Entries may have their labels displayed inline with the entry, rather than above it. This is useful for infolists with many entries, where vertical space is at a premium. To display an entry's label inline, use the `inlineLabel()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextInput::make('name')
->inlineLabel()
```
<AutoScreenshot name="infolists/entries/inline-label" alt="Infolist entry with inline label" version="4.x" />
Optionally, you may pass a boolean value to control if the label should be displayed inline or not:
```php
use Filament\Infolists\Components\TextInput;
TextInput::make('name')
->inlineLabel(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `inlineLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Using inline labels in multiple places at once
If you wish to display all labels inline in a [layout component](../schemas/layouts) like a [section](../schemas/section) or [tab](../schemas/tabs), you can use the `inlineLabel()` on the component itself, and all entries within it will have their labels displayed inline:
```php
use Filament\Infolists\Components\TextInput;
use Filament\Schemas\Components\Section;
Section::make('Details')
->inlineLabel()
->entries([
TextInput::make('name'),
TextInput::make('email')
->label('Email address'),
TextInput::make('phone')
->label('Phone number'),
])
```
<AutoScreenshot name="infolists/entries/inline-label/section" alt="Infolist entries with inline labels in a section" version="4.x" />
You can also use `inlineLabel()` on the entire schema to display all labels inline:
```php
use Filament\Schemas\Schema;
public function infolist(Schema $schema): Schema
{
return $schema
->inlineLabel()
->components([
// ...
]);
}
```
When using `inlineLabel()` on a layout component or schema, you can still opt-out of inline labels for individual entries by using the `inlineLabel(false)` method on the entry:
```php
use Filament\Infolists\Components\TextInput;
use Filament\Schemas\Components\Section;
Section::make('Details')
->inlineLabel()
->entries([
TextInput::make('name'),
TextInput::make('email')
->label('Email address'),
TextInput::make('phone')
->label('Phone number')
->inlineLabel(false),
])
```
## Adding a tooltip to an entry
You may specify a tooltip to display when you hover over an entry:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->tooltip('Shown at the top of the page')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `tooltip()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/tooltips" alt="Entry with tooltip" version="4.x" />
## Aligning entry content
You may align the content of an entry to the start (left in left-to-right interfaces, right in right-to-left interfaces), center, or end (right in left-to-right interfaces, left in right-to-left interfaces) using the `alignStart()`, `alignCenter()` or `alignEnd()` methods:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
->alignStart() // This is the default alignment.
TextEntry::make('title')
->alignCenter()
TextEntry::make('title')
->alignEnd()
```
Alternatively, you may pass an `Alignment` enum to the `alignment()` method:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\Alignment;
TextEntry::make('title')
->alignment(Alignment::Center)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `alignment()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding extra content to an entry
Entries contain many "slots" where content can be inserted in a child schema. Slots can accept text, [any schema component](../schemas), [actions](../actions) and [action groups](../actions/grouping-actions). Usually, [prime components](../schemas/primes) are used for content.
The following slots are available for all entries:
- `aboveLabel()`
- `beforeLabel()`
- `afterLabel()`
- `belowLabel()`
- `aboveContent()`
- `beforeContent()`
- `afterContent()`
- `belowContent()`
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the slot methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
To insert plain text, you can pass a string to these methods:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->belowContent('This is the user\'s full name.')
```
<AutoScreenshot name="infolists/entries/below-content/text" alt="Infolist entry with text below content" version="4.x" />
To insert a schema component, often a [prime component](../schemas/primes), you can pass the component to the method:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Text;
use Filament\Support\Enums\FontWeight;
TextEntry::make('name')
->belowContent(Text::make('This is the user\'s full name.')->weight(FontWeight::Bold))
```
<AutoScreenshot name="infolists/entries/below-content/component" alt="Infolist entry with component below content" version="4.x" />
To insert an [action](../actions) or [action group](../actions/grouping-actions), you can pass the action or action group to the method:
```php
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->belowContent(Action::make('generate'))
```
<AutoScreenshot name="infolists/entries/below-content/action" alt="Infolist entry with action below content" version="4.x" />
You can insert any combination of content into the slots by passing an array of content to the method:
```php
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->belowContent([
Icon::make(Heroicon::InformationCircle),
'This is the user\'s full name.',
Action::make('generate'),
])
```
<AutoScreenshot name="infolists/entries/below-content" alt="Infolist entry with multiple components below content" version="4.x" />
You can also align the content in the slots by passing the array of content to either `Schema::start()` (default), `Schema::end()` or `Schema::between()`:
```php
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Icon;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->belowContent(Schema::end([
Icon::make(Heroicon::InformationCircle),
'This is the user\'s full name.',
Action::make('generate'),
]))
TextEntry::make('name')
->belowContent(Schema::between([
Icon::make(Heroicon::InformationCircle),
'This is the user\'s full name.',
Action::make('generate'),
]))
TextEntry::make('name')
->belowContent(Schema::between([
Flex::make([
Icon::make(Heroicon::InformationCircle)
->grow(false),
'This is the user\'s full name.',
]),
Action::make('generate'),
]))
```
<Aside variant="tip">
As you can see in the above example for `Schema::between()`, a [`Flex` component](../schemas/layouts#flex-component) is used to group the icon and text together so they do not have space between them. The icon uses `grow(false)` to prevent it from taking up half of the horizontal space, allowing the text to consume the remaining space.
</Aside>
<AutoScreenshot name="infolists/entries/below-content/alignment" alt="Infolist entry with aligned components below content" version="4.x" />
### Adding extra content above an entry's label
You can insert extra content above an entry's label using the `aboveLabel()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->aboveLabel([
Icon::make(Heroicon::Star),
'This is the content above the entry\'s label'
])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `aboveLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/above-label" alt="Infolist entry with extra content above label" version="4.x" />
### Adding extra content before an entry's label
You can insert extra content before an entry's label using the `beforeLabel()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->beforeLabel(Icon::make(Heroicon::Star))
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `beforeLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/before-label" alt="Infolist entry with extra content before label" version="4.x" />
### Adding extra content after an entry's label
You can insert extra content after an entry's label using the `afterLabel()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->afterLabel([
Icon::make(Heroicon::Star),
'This is the content after the entry\'s label'
])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `afterLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/after-label" alt="Infolist entry with extra content after label" version="4.x" />
By default, the content in the `afterLabel()` schema is aligned to the end of the container. If you wish to align it to the start of the container, you should pass a `Schema::start()` object containing the content:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->afterLabel(Schema::start([
Icon::make(Heroicon::Star),
'This is the content after the entry\'s label'
]))
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `afterLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/after-label/aligned-start" alt="Infolist entry with extra content after label aligned to the start" version="4.x" />
### Adding extra content below an entry's label
You can insert extra content below an entry's label using the `belowLabel()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->belowLabel([
Icon::make(Heroicon::Star),
'This is the content below the entry\'s label'
])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `belowLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/below-label" alt="Infolist entry with extra content below label" version="4.x" />
<Aside variant="info">
This may seem like the same as the [`aboveContent()` method](#adding-extra-content-above-a-entries-content). However, when using [inline labels](#inline-labels), the `aboveContent()` method will place the content above the entry, not below the label, since the label is displayed in a separate column to the entry content.
</Aside>
### Adding extra content above an entry's content
You can insert extra content above an entry's content using the `aboveContent()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->aboveContent([
Icon::make(Heroicon::Star),
'This is the content above the entry\'s content'
])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `aboveContent()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/above-content" alt="Infolist entry with extra content above content" version="4.x" />
<Aside variant="info">
This may seem like the same as the [`belowLabel()` method](#adding-extra-content-below-a-entries-label). However, when using [inline labels](#inline-labels), the `belowLabel()` method will place the content below the label, not above the entry's content, since the label is displayed in a separate column to the entry content.
</Aside>
### Adding extra content before an entry's content
You can insert extra content before an entry's content using the `beforeContent()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->beforeContent(Icon::make(Heroicon::Star))
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `beforeContent()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/before-content" alt="Infolist entry with extra content before content" version="4.x" />
### Adding extra content after an entry's content
You can insert extra content after an entry's content using the `afterContent()` method. You can [pass any content](#adding-extra-content-to-a-entry) to this method, like text, a schema component, an action, or an action group:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Icon;
use Filament\Support\Icons\Heroicon;
TextEntry::make('name')
->afterContent(Icon::make(Heroicon::Star))
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `afterContent()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/after-content" alt="Infolist entry with extra content after content" version="4.x" />
## Adding extra HTML attributes to an entry
You can pass extra HTML attributes to the entry via the `extraAttributes()` method, which will be merged onto its outer HTML element. The attributes should be represented by an array, where the key is the attribute name and the value is the attribute value:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('slug')
->extraAttributes(['class' => 'bg-gray-200'])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `extraAttributes()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, calling `extraAttributes()` multiple times will overwrite the previous attributes. If you wish to merge the attributes instead, you can pass `merge: true` to the method.
### Adding extra HTML attributes to the entry wrapper
You can also pass extra HTML attributes to the very outer element of the "entry wrapper" which surrounds the label and content of the entry. This is useful if you want to style the label or spacing of the entry via CSS, since you could target elements as children of the wrapper:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('slug')
->extraEntryWrapperAttributes(['class' => 'components-locked'])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `extraEntryWrapperAttributes()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, calling `extraEntryWrapperAttributes()` multiple times will overwrite the previous attributes. If you wish to merge the attributes instead, you can pass `merge: true` to the method.
## Entry utility injection
The vast majority of methods used to configure entries accept functions as parameters instead of hardcoded values:
```php
use App\Models\User;
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->label(fn (string $state): string => str_contains($state, ' ') ? 'Full name' : 'Name')
TextEntry::make('currentUserEmail')
->state(fn (): string => auth()->user()->email)
TextEntry::make('role')
->hidden(fn (User $record): bool => $record->role === 'admin')
```
This alone unlocks many customization possibilities.
The package is also able to inject many utilities to use inside these functions, as parameters. All customization methods that accept functions as arguments can inject utilities.
These injected utilities require specific parameter names to be used. Otherwise, Filament doesn't know what to inject.
### Injecting the current state of the entry
If you wish to access the current [value (state)](#entry-content-state) of the entry, define a `$state` parameter:
```php
function ($state) {
// ...
}
```
### Injecting the state of another entry or form field
You may also retrieve the state (value) of another entry or form field from within a callback, using a `$get` parameter:
```php
use Filament\Schemas\Components\Utilities\Get;
function (Get $get) {
$email = $get('email'); // Store the value of the `email` entry in the `$email` variable.
//...
}
```
<Aside variant="tip">
Unless a form field is [reactive](../forms/overview#the-basics-of-reactivity), the schema will not refresh when the value of the field changes, only when the next user interaction occurs that makes a request to the server. If you need to react to changes in a field's value, it should be `live()`.
</Aside>
### Injecting the current Eloquent record
You may retrieve the Eloquent record for the current schema using a `$record` parameter:
```php
use Illuminate\Database\Eloquent\Model;
function (?Model $record) {
// ...
}
```
### Injecting the current operation
If you're writing a schema for a panel resource or relation manager, and you wish to check if a schema is `create`, `edit` or `view`, use the `$operation` parameter:
```php
function (string $operation) {
// ...
}
```
<Aside variant="info">
You can manually set a schema's operation using the `$schema->operation()` method.
</Aside>
### Injecting the current Livewire component instance
If you wish to access the current Livewire component instance, define a `$livewire` parameter:
```php
use Livewire\Component;
function (Component $livewire) {
// ...
}
```
### Injecting the current entry instance
If you wish to access the current component instance, define a `$component` parameter:
```php
use Filament\Infolists\Components\Entry;
function (Entry $component) {
// ...
}
```
### Injecting multiple utilities
The parameters are injected dynamically using reflection, so you are able to combine multiple parameters in any order:
```php
use App\Models\User;
use Filament\Schemas\Components\Utilities\Get;
use Livewire\Component as Livewire;
function (Livewire $livewire, Get $get, User $record) {
// ...
}
```
### Injecting dependencies from Laravel's container
You may inject anything from Laravel's container like normal, alongside utilities:
```php
use App\Models\User;
use Illuminate\Http\Request;
function (Request $request, User $record) {
// ...
}
```
## Global settings
If you wish to change the default behavior of all entries globally, then you can call the static `configureUsing()` method inside a service provider's `boot()` method, to which you pass a Closure to modify the entries using. For example, if you wish to make all `TextEntry` components [`words(10)`](text-entry#limiting-word-count), you can do it like so:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::configureUsing(function (TextEntry $entry): void {
$entry->words(10);
});
```
Of course, you are still able to overwrite this on each entry individually:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('name')
->words(null)
```
@@ -0,0 +1,745 @@
---
title: Text entry
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
Text entries display simple text:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('title')
```
<AutoScreenshot name="infolists/entries/text/simple" alt="Text entry" version="4.x" />
## Customizing the color
You may set a [color](../styling/colors) for the text:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('status')
->color('primary')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `color()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/color" alt="Text entry in the primary color" version="4.x" />
## Adding an icon
Text entries may also have an [icon](../styling/icons):
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Icons\Heroicon;
TextEntry::make('email')
->icon(Heroicon::Envelope)
```
<UtilityInjection set="infolistEntries" version="4.x">The `icon()` method also accepts a function to dynamically calculate the icon. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/icon" alt="Text entry with icon" version="4.x" />
You may set the position of an icon using `iconPosition()`:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Icons\Heroicon;
TextEntry::make('email')
->icon(Heroicon::Envelope)
->iconPosition(IconPosition::After) // `IconPosition::Before` or `IconPosition::After`
```
<UtilityInjection set="infolistEntries" version="4.x">The `iconPosition()` method also accepts a function to dynamically calculate the icon position. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/icon-after" alt="Text entry with icon after" version="4.x" />
The icon color defaults to the text color, but you may customize the icon [color](../styling/colors) separately using `iconColor()`:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Icons\Heroicon;
TextEntry::make('email')
->icon(Heroicon::Envelope)
->iconColor('primary')
```
<UtilityInjection set="infolistEntries" version="4.x">The `iconColor()` method also accepts a function to dynamically calculate the icon color. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/icon-color" alt="Text entry with icon in the primary color" version="4.x" />
## Displaying as a "badge"
By default, text is quite plain and has no background color. You can make it appear as a "badge" instead using the `badge()` method. A great use case for this is with statuses, where may want to display a badge with a [color](#customizing-the-color) that matches the status:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'reviewing' => 'warning',
'published' => 'success',
'rejected' => 'danger',
})
```
<AutoScreenshot name="infolists/entries/text/badge" alt="Text entry as badge" version="4.x" />
You may add other things to the badge, like an [icon](#adding-an-icon).
Optionally, you may pass a boolean value to control if the text should be in a badge or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('status')
->badge(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `badge()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Formatting
When using a text entry, you may want the actual outputted text in the UI to differ from the raw [state](overview#entry-content-state) of the entry, which is often automatically retrieved from an Eloquent model. Formatting the state allows you to preserve the integrity of the raw data while also allowing it to be presented in a more user-friendly way.
To format the state of a text entry without changing the state itself, you can use the `formatStateUsing()` method. This method accepts a function that takes the state as an argument and returns the formatted state:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('status')
->formatStateUsing(fn (string $state): string => __("statuses.{$state}"))
```
In this case, the `status` column in the database might contain values like `draft`, `reviewing`, `published`, or `rejected`, but the formatted state will be the translated version of these values.
<UtilityInjection set="infolistEntries" version="4.x">The function passed to `formatStateUsing()` can inject various utilities as parameters.</UtilityInjection>
### Date formatting
Instead of passing a function to `formatStateUsing()`, you may use the `date()`, `dateTime()`, and `time()` methods to format the entry's state using [PHP date formatting tokens](https://www.php.net/manual/en/datetime.format.php):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->date()
TextEntry::make('created_at')
->dateTime()
TextEntry::make('created_at')
->time()
```
You may customize the date format by passing a custom format string to the `date()`, `dateTime()`, or `time()` method. You may use any [PHP date formatting tokens](https://www.php.net/manual/en/datetime.format.php):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->date('M j, Y')
TextEntry::make('created_at')
->dateTime('M j, Y H:i:s')
TextEntry::make('created_at')
->time('H:i:s')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `date()`, `dateTime()`, and `time()` methods also accept a function to dynamically calculate the format. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Date formatting using Carbon macro formats
You may use also the `isoDate()`, `isoDateTime()`, and `isoTime()` methods to format the entry's state using [Carbon's macro-formats](https://carbon.nesbot.com/docs/#available-macro-formats):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->isoDate()
TextEntry::make('created_at')
->isoDateTime()
TextEntry::make('created_at')
->isoTime()
```
You may customize the date format by passing a custom macro format string to the `isoDate()`, `isoDateTime()`, or `isoTime()` method. You may use any [Carbon's macro-formats](https://carbon.nesbot.com/docs/#available-macro-formats):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->isoDate('L')
TextEntry::make('created_at')
->isoDateTime('LLL')
TextEntry::make('created_at')
->isoTime('LT')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `isoDate()`, `isoDateTime()`, and `isoTime()` methods also accept a function to dynamically calculate the format. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Relative date formatting
You may use the `since()` method to format the entry's state using [Carbon's `diffForHumans()`](https://carbon.nesbot.com/docs/#api-humandiff):
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->since()
```
#### Displaying a formatting date in a tooltip
Additionally, you can use the `dateTooltip()`, `dateTimeTooltip()`, `timeTooltip()`, `isoDateTooltip()`, `isoDateTimeTooltip()`, `isoTime()`, `isoTimeTooltip()`, or `sinceTooltip()` method to display a formatted date in a [tooltip](overview#adding-a-tooltip-to-an-entry), often to provide extra information:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->since()
->dateTooltip() // Accepts a custom PHP date formatting string
TextEntry::make('created_at')
->since()
->dateTimeTooltip() // Accepts a custom PHP date formatting string
TextEntry::make('created_at')
->since()
->timeTooltip() // Accepts a custom PHP date formatting string
TextEntry::make('created_at')
->since()
->isoDateTooltip() // Accepts a custom Carbon macro format string
TextEntry::make('created_at')
->since()
->isoDateTimeTooltip() // Accepts a custom Carbon macro format string
TextEntry::make('created_at')
->since()
->isoTimeTooltip() // Accepts a custom Carbon macro format string
TextEntry::make('created_at')
->dateTime()
->sinceTooltip()
```
#### Setting the timezone for date formatting
Each of the date formatting methods listed above also accepts a `timezone` argument, which allows you to convert the time set in the state to a different timezone:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->dateTime(timezone: 'America/New_York')
```
You can also pass a timezone to the `timezone()` method of the entry to apply a timezone to all date-time formatting methods at once:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('created_at')
->timezone('America/New_York')
->dateTime()
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `timezone()` method also accepts a function to dynamically calculate the timezone. You can inject various utilities into the function as parameters.</UtilityInjection>
If you do not pass a `timezone()` to the entry, it will use Filament's default timezone. You can set Filament's default timezone using the `FilamentTimezone::set()` method in the `boot()` method of a service provider such as `AppServiceProvider`:
```php
use Filament\Support\Facades\FilamentTimezone;
public function boot(): void
{
FilamentTimezone::set('America/New_York');
}
```
This is useful if you want to set a default timezone for all text entries in your application. It is also used in other places where timezones are used in Filament.
<Aside variant="warning">
Filament's default timezone will only apply when the entry stores a time. If the entry stores a date only (`date()` instead of `dateTime()`), the timezone will not be applied. This is to prevent timezone shifts when storing dates without times.
</Aside>
### Number formatting
Instead of passing a function to `formatStateUsing()`, you can use the `numeric()` method to format an entry as a number:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric()
```
If you would like to customize the number of decimal places used to format the number with, you can use the `decimalPlaces` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric(decimalPlaces: 0)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `decimalPlaces` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, your app's locale will be used to format the number suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('stock')
->numeric(locale: 'nl')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `locale` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Money formatting
Instead of passing a function to `formatStateUsing()`, you can use the `money()` method to easily format amounts of money, in any currency:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('price')
->money('EUR')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `money()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
There is also a `divideBy` argument for `money()` that allows you to divide the original value by a number before formatting it. This could be useful if your database stores the price in cents, for example:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('price')
->money('EUR', divideBy: 100)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `divideBy` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, your app's locale will be used to format the money suitably. If you would like to customize the locale used, you can pass it to the `locale` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('price')
->money('EUR', locale: 'nl')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `locale` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
If you would like to customize the number of decimal places used to format the number with, you can use the `decimalPlaces` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('price')
->money('EUR', decimalPlaces: 3)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `decimalPlaces` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Rendering Markdown
If your entry value is Markdown, you may render it using `markdown()`:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->markdown()
```
Optionally, you may pass a boolean value to control if the text should be rendered as Markdown or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->markdown(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `markdown()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Rendering HTML
If your entry value is HTML, you may render it using `html()`:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->html()
```
Optionally, you may pass a boolean value to control if the text should be rendered as HTML or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->html(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `html()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Rendering raw HTML without sanitization
If you use this method, then the HTML will be sanitized to remove any potentially unsafe content before it is rendered. If you'd like to opt out of this behavior, you can wrap the HTML in an `HtmlString` object by formatting it:
```php
use Filament\Infolists\Components\TextEntry;
use Illuminate\Support\HtmlString;
TextEntry::make('description')
->formatStateUsing(fn (string $state): HtmlString => new HtmlString($state))
```
<Aside variant="danger">
Be cautious when rendering raw HTML, as it may contain malicious content, which can lead to security vulnerabilities in your app such as cross-site scripting (XSS) attacks. Always ensure that the HTML you are rendering is safe before using this method.
</Aside>
Alternatively, you can return a `view()` object from the `formatStateUsing()` method, which will also not be sanitized:
```php
use Filament\Infolists\Components\TextEntry;
use Illuminate\Contracts\View\View;
TextEntry::make('description')
->formatStateUsing(fn (string $state): View => view(
'filament.infolists.components.description-entry-content',
['state' => $state],
))
```
## Listing multiple values
Multiple values can be rendered in a text entry if its [state](overview#entry-content-state) is an array. This can happen if you are using an `array` cast on an Eloquent attribute, an Eloquent relationship with multiple results, or if you have passed an array to the [`state()` method](overview#setting-the-state-of-an-entry). If there are multiple values inside your text entry, they will be comma-separated. You may use the `listWithLineBreaks()` method to display them on new lines instead:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->listWithLineBreaks()
```
<AutoScreenshot name="infolists/entries/text/list" alt="Text entry with multiple values" version="4.x" />
Optionally, you may pass a boolean value to control if the text should have line breaks between each item or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->listWithLineBreaks(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `listWithLineBreaks()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Adding bullet points to the list
You may add a bullet point to each list item using the `bulleted()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->bulleted()
```
<AutoScreenshot name="infolists/entries/text/bullet-list" alt="Text entry with multiple values and bullet points" version="4.x" />
Optionally, you may pass a boolean value to control if the text should have bullet points or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->bulleted(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `bulleted()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Limiting the number of values in the list
You can limit the number of values in the list using the `limitList()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->listWithLineBreaks()
->limitList(3)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `limitList()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Expanding the limited list
You can allow the limited items to be expanded and collapsed, using the `expandableLimitedList()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->listWithLineBreaks()
->limitList(3)
->expandableLimitedList()
```
<Aside variant="info">
This is only a feature for `listWithLineBreaks()` or `bulleted()`, where each item is on its own line.
</Aside>
Optionally, you may pass a boolean value to control if the text should be expandable or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('authors.name')
->listWithLineBreaks()
->limitList(3)
->expandableLimitedList(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `expandableLimitedList()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Splitting a single value into multiple list items
If you want to "explode" a text string from your model into multiple list items, you can do so with the `separator()` method. This is useful for displaying comma-separated tags [as badges](#displaying-as-a-badge), for example:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('tags')
->badge()
->separator(',')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `separator()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Aggregating relationships
Filament provides several methods for aggregating a relationship field, including `avg()`, `max()`, `min()` and `sum()`. For instance, if you wish to show the average of a field on all related records, you may use the `avg()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('users_avg_age')->avg('users', 'age')
```
In this example, `users` is the name of the relationship, while `age` is the field that is being averaged. The name of the entry must be `users_avg_age`, as this is the convention that [Laravel uses](https://laravel.com/docs/eloquent-relationships#other-aggregate-functions) for storing the result.
If you'd like to scope the relationship before aggregating, you can pass an array to the method, where the key is the relationship name and the value is the function to scope the Eloquent query with:
```php
use Filament\Infolists\Components\TextEntry;
use Illuminate\Database\Eloquent\Builder;
TextEntry::make('users_avg_age')->avg([
'users' => fn (Builder $query) => $query->where('is_active', true),
], 'age')
```
## Customizing the text size
Text entries have small font size by default, but you may change this to `TextSize::ExtraSmall`, `TextSize::Medium`, or `TextSize::Large`.
For instance, you may make the text larger using `size(TextSize::Large)`:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\TextSize;
TextEntry::make('title')
->size(TextSize::Large)
```
<AutoScreenshot name="infolists/entries/text/large" alt="Text entry in a large font size" version="4.x" />
## Customizing the font weight
Text entries have regular font weight by default, but you may change this to any of the following options: `FontWeight::Thin`, `FontWeight::ExtraLight`, `FontWeight::Light`, `FontWeight::Medium`, `FontWeight::SemiBold`, `FontWeight::Bold`, `FontWeight::ExtraBold` or `FontWeight::Black`.
For instance, you may make the font bold using `weight(FontWeight::Bold)`:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\FontWeight;
TextEntry::make('title')
->weight(FontWeight::Bold)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `weight()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/bold" alt="Text entry in a bold font" version="4.x" />
## Customizing the font family
You can change the text font family to any of the following options: `FontFamily::Sans`, `FontFamily::Serif` or `FontFamily::Mono`.
For instance, you may make the font monospaced using `fontFamily(FontFamily::Mono)`:
```php
use Filament\Support\Enums\FontFamily;
use Filament\Infolists\Components\TextEntry;
TextEntry::make('apiKey')
->label('API key')
->fontFamily(FontFamily::Mono)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `fontFamily()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/text/mono" alt="Text entry in a monospaced font" version="4.x" />
## Handling long text
### Limiting text length
You may `limit()` the length of the entry's value:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->limit(50)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `limit()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, when text is truncated, an ellipsis (`...`) is appended to the end of the text. You may customize this by passing a custom string to the `end` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->limit(50, end: ' (more)')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `end` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
You may also reuse the value that is being passed to `limit()` in a function, by getting it using the `getCharacterLimit()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->limit(50)
->tooltip(function (TextEntry $component): ?string {
$state = $component->getState();
if (strlen($state) <= $component->getCharacterLimit()) {
return null;
}
// Only render the tooltip if the entry contents exceeds the length limit.
return $state;
})
```
### Limiting word count
You may limit the number of `words()` displayed in the entry:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->words(10)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `words()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, when text is truncated, an ellipsis (`...`) is appended to the end of the text. You may customize this by passing a custom string to the `end` argument:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->words(10, end: ' (more)')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `end` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Limiting text to a specific number of lines
You may want to limit text to a specific number of lines instead of limiting it to a fixed length. Clamping text to a number of lines is useful in responsive interfaces where you want to ensure a consistent experience across all screen sizes. This can be achieved using the `lineClamp()` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->lineClamp(2)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `lineClamp()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing text wrapping
By default, text will wrap to the next line if it exceeds the width of the container. You can prevent this behavior using the `wrap(false)` method:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('description')
->wrap(false)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `wrap()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Allowing the text to be copied to the clipboard
You may make the text copyable, such that clicking on the entry copies the text to the clipboard, and optionally specify a custom confirmation message and duration in milliseconds:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('apiKey')
->label('API key')
->copyable()
->copyMessage('Copied!')
->copyMessageDuration(1500)
```
<AutoScreenshot name="infolists/entries/text/copyable" alt="Text entry with a button to copy it" version="4.x" />
Optionally, you may pass a boolean value to control if the text should be copyable or not:
```php
use Filament\Infolists\Components\TextEntry;
TextEntry::make('apiKey')
->label('API key')
->copyable(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `copyable()`, `copyMessage()`, and `copyMessageDuration()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="warning">
This feature only works when SSL is enabled for the app.
</Aside>
@@ -0,0 +1,131 @@
---
title: Icon entry
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
Icon entries render an [icon](../styling/icons) representing the state of the entry:
```php
use Filament\Infolists\Components\IconEntry;
use Filament\Support\Icons\Heroicon;
IconEntry::make('status')
->icon(fn (string $state): Heroicon => match ($state) {
'draft' => Heroicon::OutlinedPencil,
'reviewing' => Heroicon::OutlinedClock,
'published' => Heroicon::OutlinedCheckCircle,
})
```
<UtilityInjection set="infolistEntries" version="4.x">The `icon()` method can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/icon/simple" alt="Icon entry" version="4.x" />
## Customizing the color
You may change the [color](../styling/colors) of the icon, using the `color()` method:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('status')
->color('success')
```
By passing a function to `color()`, you can customize the color based on the state of the entry:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('status')
->color(fn (string $state): string => match ($state) {
'draft' => 'info',
'reviewing' => 'warning',
'published' => 'success',
default => 'gray',
})
```
<UtilityInjection set="infolistEntries" version="4.x">The `color()` method can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/icon/color" alt="Icon entry with color" version="4.x" />
## Customizing the size
The default icon size is `IconSize::Large`, but you may customize the size to be either `IconSize::ExtraSmall`, `IconSize::Small`, `IconSize::Medium`, `IconSize::ExtraLarge` or `IconSize::TwoExtraLarge`:
```php
use Filament\Infolists\Components\IconEntry;
use Filament\Support\Enums\IconSize;
IconEntry::make('status')
->size(IconSize::Medium)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `size()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/icon/medium" alt="Medium-sized icon entry" version="4.x" />
## Handling booleans
Icon entries can display a check or "X" icon based on the state of the entry, either true or false, using the `boolean()` method:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_featured')
->boolean()
```
> If this attribute in the model class is already cast as a `bool` or `boolean`, Filament is able to detect this, and you do not need to use `boolean()` manually.
<AutoScreenshot name="infolists/entries/icon/boolean" alt="Icon entry to display a boolean" version="4.x" />
Optionally, you may pass a boolean value to control if the icon should be boolean or not:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_featured')
->boolean(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `boolean()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing the boolean icons
You may customize the [icon](../styling/icons) representing each state:
```php
use Filament\Infolists\Components\IconEntry;
use Filament\Support\Icons\Heroicon;
IconEntry::make('is_featured')
->boolean()
->trueIcon(Heroicon::OutlinedCheckBadge)
->falseIcon(Heroicon::OutlinedXMark)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `trueIcon()` and `falseIcon()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/icon/boolean-icon" alt="Icon entry to display a boolean with custom icons" version="4.x" />
### Customizing the boolean colors
You may customize the icon [color](../styling/colors) representing each state:
```php
use Filament\Infolists\Components\IconEntry;
IconEntry::make('is_featured')
->boolean()
->trueColor('info')
->falseColor('warning')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `trueColor()` and `falseColor()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/icon/boolean-color" alt="Icon entry to display a boolean with custom colors" version="4.x" />
@@ -0,0 +1,290 @@
---
title: Image entry
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
Infolists can render images, based on the path in the state of the entry:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('header_image')
```
In this case, the `header_image` state could contain `posts/header-images/4281246003439.jpg`, which is relative to the root directory of the storage disk. The storage disk is defined in the [configuration file](../introduction/installation#publishing-configuration), `local` by default. You can also set the `FILESYSTEM_DISK` environment variable to change this.
Alternatively, the state could contain an absolute URL to an image, such as `https://example.com/images/header.jpg`.
<AutoScreenshot name="infolists/entries/image/simple" alt="Image entry" version="4.x" />
## Managing the image disk
The default storage disk is defined in the [configuration file](../introduction/installation#publishing-configuration), `local` by default. You can also set the `FILESYSTEM_DISK` environment variable to change this. If you want to deviate from the default disk, you may pass a custom disk name to the `disk()` method:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('header_image')
->disk('s3')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `disk()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Public images
By default, Filament will generate temporary URLs to images in the filesystem, unless the [disk](#managing-the-image-disk) is set to `public`. If your images are stored in a public disk, you can set the `visibility()` to `public`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('header_image')
->visibility('public')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `visibility()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Customizing the size
You may customize the image size by passing a `imageWidth()` and `imageHeight()`, or both with `imageSize()`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('header_image')
->imageWidth(200)
ImageEntry::make('header_image')
->imageHeight(50)
ImageEntry::make('author.avatar')
->imageSize(40)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static values, the `imageWidth()`, `imageHeight()` and `imageSize()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Square images
You may display the image using a 1:1 aspect ratio:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('author.avatar')
->imageHeight(40)
->square()
```
<AutoScreenshot name="infolists/entries/image/square" alt="Square image entry" version="4.x" />
Optionally, you may pass a boolean value to control if the image should be square or not:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('author.avatar')
->imageHeight(40)
->square(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `square()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Circular images
You may make the image fully rounded, which is useful for rendering avatars:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('author.avatar')
->imageHeight(40)
->circular()
```
<AutoScreenshot name="infolists/entries/image/circular" alt="Circular image entry" version="4.x" />
Optionally, you may pass a boolean value to control if the image should be circular or not:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('author.avatar')
->imageHeight(40)
->circular(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `circular()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding a default image URL
You can display a placeholder image if one doesn't exist yet, by passing a URL to the `defaultImageUrl()` method:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('header_image')
->defaultImageUrl(url('storage/posts/header-images/default.jpg'))
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `defaultImageUrl()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Stacking images
You may display multiple images as a stack of overlapping images by using `stacked()`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
```
<AutoScreenshot name="infolists/entries/image/stacked" alt="Stacked image entry" version="4.x" />
Optionally, you may pass a boolean value to control if the images should be stacked or not:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `stacked()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing the stacked ring width
The default ring width is `3`, but you may customize it to be from `0` to `8`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->ring(5)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `ring()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing the stacked overlap
The default overlap is `4`, but you may customize it to be from `0` to `8`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->overlap(2)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `overlap()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting a limit
You may limit the maximum number of images you want to display by passing `limit()`:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->limit(3)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `limit()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/image/limited" alt="Limited image entry" version="4.x" />
### Showing the remaining images count
When you set a limit you may also display the count of remaining images by passing `limitedRemainingText()`.
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->limit(3)
->limitedRemainingText()
```
<AutoScreenshot name="infolists/entries/image/limited-remaining-text" alt="Limited image entry with remaining text" version="4.x" />
Optionally, you may pass a boolean value to control if the remaining text should be displayed or not:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->limit(3)
->limitedRemainingText(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `limitedRemainingText()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Customizing the limited remaining text size
By default, the size of the remaining text is `TextSize::Small`. You can customize this to be `TextSize::ExtraSmall`, `TextSize::Medium` or `TextSize::Large` using the `size` parameter:
```php
use Filament\Infolists\Components\ImageEntry;
use Filament\Support\Enums\TextSize;
ImageEntry::make('colleagues.avatar')
->imageHeight(40)
->circular()
->stacked()
->limit(3)
->limitedRemainingText(size: TextSize::Large)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `limitedRemainingText()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Prevent file existence checks
When the schema is loaded, it will automatically detect whether the images exist to prevent errors for missing files. This is all done on the backend. When using remote storage with many images, this can be time-consuming. You can use the `checkFileExistence(false)` method to disable this feature:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('attachment')
->checkFileExistence(false)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `checkFileExistence()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding extra HTML attributes to the image
You can pass extra HTML attributes to the `<img>` element via the `extraImgAttributes()` method. The attributes should be represented by an array, where the key is the attribute name and the value is the attribute value:
```php
use Filament\Infolists\Components\ImageEntry;
ImageEntry::make('logo')
->extraImgAttributes([
'alt' => 'Logo',
'loading' => 'lazy',
])
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `extraImgAttributes()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
By default, calling `extraImgAttributes()` multiple times will overwrite the previous attributes. If you wish to merge the attributes instead, you can pass `merge: true` to the method.
@@ -0,0 +1,43 @@
---
title: Color entry
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The color entry allows you to show the color preview from a CSS color definition, typically entered using the [color picker field](../forms/color-picker), in one of the supported formats (HEX, HSL, RGB, RGBA).
```php
use Filament\Infolists\Components\ColorEntry;
ColorEntry::make('color')
```
<AutoScreenshot name="infolists/entries/color/simple" alt="Color entry" version="4.x" />
## Allowing the color to be copied to the clipboard
You may make the color copyable, such that clicking on the preview copies the CSS value to the clipboard, and optionally specify a custom confirmation message and duration in milliseconds. This feature only works when SSL is enabled for the app.
```php
use Filament\Infolists\Components\ColorEntry;
ColorEntry::make('color')
->copyable()
->copyMessage('Copied!')
->copyMessageDuration(1500)
```
<AutoScreenshot name="infolists/entries/color/copyable" alt="Color entry with a button to copy it" version="4.x" />
Optionally, you may pass a boolean value to control if the color should be copyable or not:
```php
use Filament\Infolists\Components\ColorEntry;
ColorEntry::make('color')
->copyable(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `copyable()`, `copyMessage()`, and `copyMessageDuration()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,85 @@
---
title: Code entry
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The code entry allows you to present a highlighted code snippet in your infolist. It uses [Phiki](https://github.com/phikiphp/phiki) for code highlighting on the server:
```php
use Filament\Infolists\Components\CodeEntry;
use Phiki\Grammar\Grammar;
CodeEntry::make('code')
->grammar(Grammar::Php)
```
<AutoScreenshot name="infolists/entries/code/simple" alt="Code entry" version="4.x" />
To use the code entry, you need to first install the [`phiki/phiki`](https://github.com/phikiphp/phiki) Composer package. Filament does not include it by default to allow you to choose which major version of Phiki to use explicitly, since major versions can have different grammars and themes available. You can install the latest version of Phiki using the following command:
```bash
composer require phiki/phiki
```
## Changing the code's grammar (language)
You may change the grammar (language) of the code using the `grammar()` method. Over 200 grammars are available, and you can open the `Phiki\Grammar\Grammar` enum class to see the full list. To switch to use JavaScript as the grammar, you can use the `Grammar::Javascript` enum value:
```php
use Filament\Infolists\Components\CodeEntry;
use Phiki\Grammar\Grammar;
CodeEntry::make('code')
->grammar(Grammar::Javascript)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `grammar()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="tip">
If your code entry's content is a PHP array, it will automatically be converted to a JSON string, and the grammar will be set to `Grammar::Json`.
</Aside>
## Changing the code's theme (highlighting)
You may change the theme of the code using the `lightTheme()` and `darkTheme()` methods. Over 50 themes are available, and you can open the `Phiki\Theme\Theme` enum class to see the full list. To use the popular `Dracula` theme, you can use the `Theme::Dracula` enum value:
```php
use Filament\Infolists\Components\CodeEntry;
use Phiki\Theme\Theme;
CodeEntry::make('code')
->lightTheme(Theme::Dracula)
->darkTheme(Theme::Dracula)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `lightTheme()` and `darkTheme()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/code/dracula" alt="Code entry with the Dracula theme" version="4.x" />
## Allowing the code to be copied to the clipboard
You may make the code copyable, such that clicking on it copies the code to the clipboard, and optionally specify a custom confirmation message and duration in milliseconds. This feature only works when SSL is enabled for the app.
```php
use Filament\Infolists\Components\CodeEntry;
CodeEntry::make('code')
->copyable()
->copyMessage('Copied!')
->copyMessageDuration(1500)
```
Optionally, you may pass a boolean value to control if the code should be copyable or not:
```php
use Filament\Infolists\Components\CodeEntry;
CodeEntry::make('code')
->copyable(FeatureFlag::active())
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing static values, the `copyable()`, `copyMessage()`, and `copyMessageDuration()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,74 @@
---
title: Key-value entry
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The key-value entry allows you to render key-value pairs of data, from a one-dimensional JSON object / PHP array.
```php
use Filament\Infolists\Components\KeyValueEntry;
KeyValueEntry::make('meta')
```
For example, the state of this entry might be represented as:
```php
[
'description' => 'Filament is a collection of Laravel packages',
'og:type' => 'website',
'og:site_name' => 'Filament',
]
```
<AutoScreenshot name="infolists/entries/key-value/simple" alt="Key-value entry" version="4.x" />
If you're saving the data in Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'meta' => 'array',
];
}
// ...
}
```
## Customizing the key column's label
You may customize the label for the key column using the `keyLabel()` method:
```php
use Filament\Infolists\Components\KeyValueEntry;
KeyValueEntry::make('meta')
->keyLabel('Property name')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `keyLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Customizing the value column's label
You may customize the label for the value column using the `valueLabel()` method:
```php
use Filament\Infolists\Components\KeyValueEntry;
KeyValueEntry::make('meta')
->valueLabel('Property value')
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `valueLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,145 @@
---
title: Repeatable entry
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The repeatable entry allows you to repeat a set of entries and layout components for items in an array or relationship.
```php
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
RepeatableEntry::make('comments')
->schema([
TextEntry::make('author.name'),
TextEntry::make('title'),
TextEntry::make('content')
->columnSpan(2),
])
->columns(2)
```
As you can see, the repeatable entry has an embedded `schema()` which gets repeated for each item.
For example, the state of this entry might be represented as:
```php
[
[
'author' => ['name' => 'Jane Doe'],
'title' => 'Wow!',
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod, nisl eget aliquam ultricies, nunc nisl aliquet nunc, quis aliquam nisl.',
],
[
'author' => ['name' => 'John Doe'],
'title' => 'This isn\'t working. Help!',
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod, nisl eget aliquam ultricies, nunc nisl aliquet nunc, quis aliquam nisl.',
],
]
```
<AutoScreenshot name="infolists/entries/repeatable/simple" alt="Repeatable entry" version="4.x" />
Alternatively, `comments` and `author` could be Eloquent relationships, `title` and `content` could be attributes on the comment model, and `name` could be an attribute on the author model. Filament will automatically handle the relationship loading and display the data in the same way.
## Grid layout
You may organize repeatable items into columns by using the `grid()` method:
```php
use Filament\Infolists\Components\RepeatableEntry;
RepeatableEntry::make('comments')
->schema([
// ...
])
->grid(2)
```
This method accepts the same options as the `columns()` method of the [grid](../schemas/layouts#grid-system). This allows you to responsively customize the number of grid columns at various breakpoints.
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `grid()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="infolists/entries/repeatable/grid" alt="Repeatable entry in grid layout" version="4.x" />
## Removing the styled container
By default, each item in a repeatable entry is wrapped in a container styled as a card. You may remove the styled container using `contained()`:
```php
use Filament\Infolists\Components\RepeatableEntry;
RepeatableEntry::make('comments')
->schema([
// ...
])
->contained(false)
```
<UtilityInjection set="infolistEntries" version="4.x">As well as allowing a static value, the `contained()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Table repeatable layout
You can present repeatable items in a table format using the `table()` method, which accepts an array of `TableColumn` objects. These objects represent the columns of the table, which correspond to any components in the schema of the entry:
```php
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
use Filament\Infolists\Components\TextEntry;
RepeatableEntry::make('comments')
->table([
TableColumn::make('Author'),
TableColumn::make('Title'),
TableColumn::make('Published'),
])
->schema([
TextEntry::make('author.name'),
TextEntry::make('title'),
IconEntry::make('is_published')
->boolean(),
])
```
<AutoScreenshot name="infolists/entries/repeatable/table" alt="Repeatable entry with table layout" version="4.x" />
The labels displayed in the header of the table are passed to the `TableColumn::make()` method. If you want to provide an accessible label for a column but do not wish to display it, you can use the `hiddenHeaderLabel()` method:
```php
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
TableColumn::make('Name')
->hiddenHeaderLabel()
```
You can enable wrapping of the column header using the `wrapHeader()` method:
```php
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
TableColumn::make('Name')
->wrapHeader()
```
You can also adjust the alignment of the column header using the `alignment()` method, passing an `Alignment` option of `Alignment::Start`, `Alignment::Center`, or `Alignment::End`:
```php
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
use Filament\Support\Enums\Alignment;
TableColumn::make('Name')
->alignment(Alignment::Center)
```
You can set a fixed column width using the `width()` method, passing a string value that represents the width of the column. This value is passed directly to the `style` attribute of the column header:
```php
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
TableColumn::make('Name')
->width('200px')
```
@@ -0,0 +1,211 @@
---
title: Custom entries
---
import Aside from "@components/Aside.astro"
## Introduction
You may create your own custom entry classes and views, which you can reuse across your project, and even release as a plugin to the community.
To create a custom entry class and view, you may use the following command:
```bash
php artisan make:filament-infolist-entry AudioPlayerEntry
```
This will create the following component class:
```php
use Filament\Infolists\Components\Entry;
class AudioPlayerEntry extends Entry
{
protected string $view = 'filament.infolists.components.audio-player-entry';
}
```
It will also create a view file at `resources/views/filament/infolists/components/audio-player-entry.blade.php`.
<Aside variant="info">
Filament infolist entries are **not** Livewire components. Defining public properties and methods on a infolist entry class will not make them accessible in the Blade view.
</Aside>
## Accessing the state of the entry in the Blade view
Inside the Blade view, you may access the [state](overview#entry-content-state) of the entry using the `$getState()` function:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
{{ $getState() }}
</x-dynamic-component>
```
## Accessing the state of another component in the Blade view
Inside the Blade view, you may access the state of another component in the schema using the `$get()` function:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
{{ $get('email') }}
</x-dynamic-component>
```
<Aside variant="tip">
Unless a form field is [reactive](../infolists/overview#the-basics-of-reactivity), the Blade view will not refresh when the value of the field changes, only when the next user interaction occurs that makes a request to the server. If you need to react to changes in a field's value, it should be `live()`.
</Aside>
## Accessing the Eloquent record in the Blade view
Inside the Blade view, you may access the current Eloquent record using the `$record` variable:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
{{ $record->name }}
</x-dynamic-component>
```
## Accessing the current operation in the Blade view
Inside the Blade view, you may access the current operation, usually `create`, `edit` or `view`, using the `$operation` variable:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
@if ($operation === 'create')
This is a new conference.
@else
This is an existing conference.
@endif
</x-dynamic-component>
```
## Accessing the current Livewire component instance in the Blade view
Inside the Blade view, you may access the current Livewire component instance using `$this`:
```blade
@php
use Filament\Resources\Users\RelationManagers\ConferencesRelationManager;
@endphp
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
@if ($this instanceof ConferencesRelationManager)
You are editing conferences the of a user.
@endif
</x-dynamic-component>
```
## Accessing the current entry instance in the Blade view
Inside the Blade view, you may access the current entry instance using `$entry`. You can call public methods on this object to access other information that may not be available in variables:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
@if ($entry->isLabelHidden())
This is a new conference.
@endif
</x-dynamic-component>
```
## Adding a configuration method to a custom entry class
You may add a public method to the custom entry class that accepts a configuration value, stores it in a protected property, and returns it again from another public method:
```php
use Filament\Infolists\Components\Entry;
class AudioPlayerEntry extends Entry
{
protected string $view = 'filament.infolists.components.audio-player-entry';
protected ?float $speed = null;
public function speed(?float $speed): static
{
$this->speed = $speed;
return $this;
}
public function getSpeed(): ?float
{
return $this->speed;
}
}
```
Now, in the Blade view for the custom entry, you may access the speed using the `$getSpeed()` function:
```blade
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
{{ $getSpeed() }}
</x-dynamic-component>
```
Any public method that you define on the custom entry class can be accessed in the Blade view as a variable function in this way.
To pass the configuration value to the custom entry class, you may use the public method:
```php
use App\Filament\Infolists\Components\AudioPlayerEntry;
AudioPlayerEntry::make('recording')
->speed(0.5)
```
## Allowing utility injection in a custom entry configuration method
[Utility injection](overview#entry-utility-injection) is a powerful feature of Filament that allows users to configure a component using functions that can access various utilities. You can allow utility injection by ensuring that the parameter type and property type of the configuration allows the user to pass a `Closure`. In the getter method, you should pass the configuration value to the `$this->evaluate()` method, which will inject utilities into the user's function if they pass one, or return the value if it is static:
```php
use Closure;
use Filament\Infolists\Components\Entry;
class AudioPlayerEntry extends Entry
{
protected string $view = 'filament.infolists.components.audio-player-entry';
protected float | Closure | null $speed = null;
public function speed(float | Closure | null $speed): static
{
$this->speed = $speed;
return $this;
}
public function getSpeed(): ?float
{
return $this->evaluate($this->speed);
}
}
```
Now, you can pass a static value or a function to the `speed()` method, and [inject any utility](overview#component-utility-injection) as a parameter:
```php
use App\Filament\Infolists\Components\AudioPlayerEntry;
AudioPlayerEntry::make('recording')
->speed(fn (Conference $record): float => $record->isGlobal() ? 1 : 0.5)
```
@@ -0,0 +1,20 @@
.fi-in-code {
& .phiki {
@apply overflow-x-auto rounded-lg px-4 py-3 shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20;
}
@variant dark {
& .phiki,
& .phiki span {
color: var(--phiki-dark-color) !important;
background-color: var(--phiki-dark-background-color) !important;
font-style: var(--phiki-dark-font-style) !important;
font-weight: var(--phiki-dark-font-weight) !important;
text-decoration: var(--phiki-dark-text-decoration) !important;
}
}
&.fi-copyable {
@apply cursor-pointer;
}
}
@@ -0,0 +1,34 @@
.fi-in-color {
@apply flex w-full gap-1.5;
&.fi-wrapped {
@apply flex-wrap;
}
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-center {
@apply justify-center;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
&.fi-align-justify,
&.fi-align-between {
@apply justify-between;
}
& > .fi-in-color-item {
@apply size-6 rounded-md;
&.fi-copyable {
@apply cursor-pointer;
}
}
}
@@ -0,0 +1,68 @@
.fi-in-entry {
@apply grid gap-y-2;
&.fi-in-entry-has-inline-label {
@apply sm:grid-cols-3 sm:items-start sm:gap-x-4;
& .fi-in-entry-content-col {
@apply sm:col-span-2;
}
}
& .fi-in-entry-label-ctn {
@apply flex items-start gap-x-3;
& > .fi-sc:nth-child(1) {
@apply grow-0;
}
}
& .fi-in-entry-label {
@apply text-sm font-medium text-gray-950 dark:text-white;
&.fi-hidden {
@apply hidden;
}
}
& .fi-in-entry-label-col {
@apply grid auto-cols-fr gap-y-2;
}
& .fi-in-entry-content-col {
@apply grid auto-cols-fr gap-y-2;
}
& .fi-in-entry-content-ctn {
@apply flex w-full items-center gap-x-3;
}
& .fi-in-entry-content {
@apply block w-full text-start;
&.fi-align-center {
@apply text-center;
}
&.fi-align-end {
@apply text-end;
}
&.fi-align-left {
@apply text-left;
}
&.fi-align-right {
@apply text-right;
}
&.fi-align-justify,
&.fi-align-between {
@apply text-justify;
}
}
& .fi-in-placeholder {
@apply text-sm text-gray-400 dark:text-gray-500;
}
}
@@ -0,0 +1,38 @@
.fi-in-icon {
@apply flex w-full gap-1.5;
&.fi-wrapped {
@apply flex-wrap;
}
&.fi-in-icon-has-line-breaks {
@apply flex-col;
}
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-center {
@apply justify-center;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
&.fi-align-justify,
&.fi-align-between {
@apply justify-between;
}
& > .fi-icon {
@apply text-gray-400 dark:text-gray-500;
&.fi-color {
@apply text-(--text) dark:text-(--dark-text);
}
}
}
@@ -0,0 +1,119 @@
.fi-in-image {
@apply flex w-full items-center gap-1.5;
& img {
@apply max-w-none object-cover object-center;
}
&.fi-circular {
& img {
@apply rounded-full;
}
}
&.fi-in-image-ring {
& img,
& .fi-in-image-limited-remaining-text {
@apply ring ring-white dark:ring-gray-900;
}
&.fi-in-image-ring-1 {
& img,
& .fi-in-image-limited-remaining-text {
@apply ring-1;
}
}
&.fi-in-image-ring-2 {
& img,
& .fi-in-image-limited-remaining-text {
@apply ring-2;
}
}
&.fi-in-image-ring-4 {
& img,
& .fi-in-image-limited-remaining-text {
@apply ring-4;
}
}
}
&.fi-in-image-overlap-1 {
@apply gap-x-0 -space-x-1;
}
&.fi-in-image-overlap-2 {
@apply gap-x-0 -space-x-2;
}
&.fi-in-image-overlap-3 {
@apply gap-x-0 -space-x-3;
}
&.fi-in-image-overlap-4 {
@apply gap-x-0 -space-x-4;
}
&.fi-in-image-overlap-5 {
@apply gap-x-0 -space-x-5;
}
&.fi-in-image-overlap-6 {
@apply gap-x-0 -space-x-6;
}
&.fi-in-image-overlap-7 {
@apply gap-x-0 -space-x-7;
}
&.fi-in-image-overlap-8 {
@apply gap-x-0 -space-x-8;
}
&.fi-wrapped {
@apply flex-wrap;
}
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-center {
@apply justify-center;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
&.fi-align-justify,
&.fi-align-between {
@apply justify-between;
}
&.fi-stacked {
& .fi-in-image-limited-remaining-text {
@apply rounded-full bg-gray-100 dark:bg-gray-800;
}
}
& .fi-in-image-limited-remaining-text {
@apply flex items-center justify-center text-sm font-medium text-gray-500 dark:text-gray-400;
&.fi-size-xs {
@apply text-xs;
}
&.fi-size-base,
&.fi-size-md {
@apply text-base;
}
&.fi-size-lg {
@apply text-lg;
}
}
}
@@ -0,0 +1,23 @@
.fi-in-key-value {
@apply w-full table-auto divide-y divide-gray-200 rounded-lg bg-white shadow-xs ring-1 ring-gray-950/5 dark:divide-white/5 dark:bg-white/5 dark:ring-white/10;
& th {
@apply px-3 py-2 text-start text-sm font-medium text-gray-700 dark:text-gray-200;
}
& tbody {
@apply divide-y divide-gray-200 font-mono text-base sm:text-sm sm:leading-6 dark:divide-white/5;
}
& tr {
@apply divide-x divide-gray-200 rtl:divide-x-reverse dark:divide-white/5;
}
& td {
@apply w-1/2 px-3 py-1.5 wrap-anywhere;
&.fi-in-placeholder {
@apply w-full py-2 text-center font-sans;
}
}
}
@@ -0,0 +1,144 @@
.fi-in-repeatable {
ul& {
@apply gap-4;
}
& .fi-in-repeatable-item {
@apply block;
}
&.fi-contained {
& .fi-in-repeatable-item {
@apply rounded-xl bg-white p-4 shadow-xs ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10;
}
}
}
.fi-in-table-repeatable {
@apply grid gap-3;
& > table {
@apply block w-full divide-y divide-gray-200 rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10;
& > thead {
@apply hidden whitespace-nowrap;
& > tr {
& > th {
@apply border-gray-200 bg-gray-50 px-3 py-2 text-start text-sm font-semibold text-gray-950 first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl dark:border-white/5 dark:bg-white/5 dark:text-white [&:not(:first-of-type)]:border-s [&:not(:last-of-type)]:border-e;
&.fi-align-center {
@apply text-center;
}
&.fi-align-end,
&.fi-align-right {
@apply text-end;
}
&.fi-wrapped {
@apply whitespace-normal;
}
&:not(.fi-wrapped) {
@apply whitespace-nowrap;
}
&.fi-in-table-repeatable-empty-header-cell {
@apply w-1;
}
}
}
}
& > tbody {
@apply block divide-y divide-gray-200 dark:divide-white/5;
& > tr {
@apply grid gap-6 p-6;
& > td {
@apply block;
&.fi-hidden {
@apply hidden;
}
}
}
}
}
@supports (container-type: inline-size) {
@apply @container;
& > table {
@apply @xl:table;
& > thead {
@apply @xl:table-header-group;
}
& > tbody {
@apply @xl:table-row-group;
& > tr {
@apply @xl:table-row @xl:p-0;
& > td {
@apply @xl:table-cell @xl:px-3 @xl:py-2;
&.fi-hidden {
@apply @xl:table-cell;
}
& .fi-in-entry {
@apply @xl:gap-y-0;
}
& .fi-in-entry-label {
@apply @xl:hidden;
}
}
}
}
& .fi-in-table-repeatable-actions {
@apply @xl:px-3 @xl:py-2;
}
}
}
@supports not (container-type: inline-size) {
& > table {
@apply lg:table;
& > thead {
@apply lg:table-header-group;
}
& > tbody {
@apply lg:table-row-group;
& > tr {
@apply lg:table-row lg:p-0;
& > td {
@apply lg:table-cell lg:px-3 lg:py-2;
&.fi-hidden {
@apply lg:table-cell;
}
& .fi-in-entry {
@apply lg:gap-y-0;
}
& .fi-in-entry-label {
@apply lg:hidden;
}
}
}
}
}
}
}
@@ -0,0 +1,228 @@
.fi-in-text {
@apply w-full;
&.fi-in-text-affixed {
@apply flex gap-3;
}
& .fi-in-text-affixed-content {
@apply min-w-0 flex-1;
}
& .fi-in-text-affix {
@apply flex items-center gap-3 self-stretch;
}
&.fi-in-text-list-limited {
/* Add space between content and list limit message. */
@apply flex flex-col;
&.fi-in-text-has-badges {
/* When the content contains badges, more gap is required to balance the spacing. */
@apply gap-y-2;
}
&:not(.fi-in-text-has-badges) {
/* When the content does not contain badges, less gap is required to balance the spacing. */
@apply gap-y-1;
}
}
ul&.fi-bulleted,
&.fi-bulleted ul {
@apply list-inside list-disc;
}
&:not(.fi-in-text-has-line-breaks) {
ul&.fi-in-text-has-badges,
&.fi-in-text-has-badges ul {
/* A list of badges without line breaks need to remain on the same line, not wrap */
@apply flex gap-x-1.5;
&.fi-wrapped,
&:is(.fi-wrapped ul) {
/* When wrap is enabled, some badges can remain on the same line as others and some should wrap */
@apply flex-wrap gap-y-1;
}
}
}
ul&.fi-in-text-has-badges,
&.fi-in-text-has-badges ul {
&.fi-in-text-has-line-breaks,
&:is(.fi-in-text-has-line-breaks ul) {
/* Add vertical gap between badges in a list with line breaks */
@apply flex flex-col gap-y-1;
}
&:not(ul.fi-in-text-has-line-breaks),
&:not(.fi-in-text-has-line-breaks ul) {
/* A list of badges without line breaks need to remain on the same line, not wrap */
@apply flex gap-x-1.5;
&.fi-wrapped,
&:is(.fi-wrapped ul) {
/* When wrap is enabled, some badges can remain on the same line as others and some should wrap */
@apply flex-wrap gap-y-1;
}
}
}
&.fi-wrapped:not(.fi-in-text-has-badges.fi-in-text-has-line-breaks) {
/* If the content has badges, and they have line breaks, setting `whitespace-normal` sometimes pushes the badge
onto the line below the bullet point if there is one, and badge content does not need to wrap anyway. */
@apply whitespace-normal;
@apply wrap-break-word;
& .fi-badge,
& .fi-in-text-list-limited-message {
/* Badge content and list limited message and actions should not wrap. */
@apply whitespace-nowrap;
}
}
& > .fi-in-text-list-limited-message {
@apply text-sm text-gray-500 dark:text-gray-400;
}
&.fi-align-center {
@apply text-center;
ul&,
& ul {
@apply justify-center;
}
}
&.fi-align-end,
&.fi-align-right {
@apply text-end;
ul&,
& ul {
@apply justify-end;
}
}
&.fi-align-justify,
&.fi-align-between {
@apply text-justify;
ul&,
& ul {
@apply justify-between;
}
}
}
.fi-in-text-item {
@apply text-gray-950 dark:text-white;
& a {
@apply hover:underline focus-visible:underline;
}
&:not(.fi-bulleted li.fi-in-text-item) {
/* Line clamping sets the `display` property of the list item to `-webkit-box` instead of `list-item`, which hides the bullet point. */
@apply line-clamp-(--line-clamp,none);
}
& > .fi-copyable {
@apply cursor-pointer;
}
&.fi-size-xs {
@apply text-xs;
}
&.fi-size-sm {
@apply text-sm;
}
&.fi-size-md {
@apply text-base;
}
&.fi-size-lg {
@apply text-lg;
}
&.fi-font-thin {
@apply font-thin;
}
&.fi-font-extralight {
@apply font-extralight;
}
&.fi-font-light {
@apply font-light;
}
&.fi-font-normal {
@apply font-normal;
}
&.fi-font-medium {
@apply font-medium;
}
&.fi-font-semibold {
@apply font-semibold;
}
&.fi-font-bold {
@apply font-bold;
}
&.fi-font-extrabold {
@apply font-extrabold;
}
&.fi-font-black {
@apply font-black;
}
&.fi-font-sans {
@apply font-sans;
}
&.fi-font-serif {
@apply font-serif;
}
&.fi-font-mono {
@apply font-mono;
}
&.fi-color {
@apply text-(--text) dark:text-(--dark-text);
li&::marker {
@apply text-gray-950;
}
@variant dark {
li&::marker {
@apply text-white;
}
}
}
&.fi-color-gray {
@apply text-gray-500 dark:text-gray-400;
li&::marker {
@apply text-gray-950;
}
}
& > .fi-icon,
& > span:not(.fi-badge) > .fi-icon {
@apply inline-block shrink-0 text-gray-400 dark:text-gray-500;
&.fi-color {
@apply text-color-500;
}
}
}
@@ -0,0 +1,8 @@
@import './components/code.css' layer(components);
@import './components/color.css' layer(components);
@import './components/entry.css' layer(components);
@import './components/key-value.css' layer(components);
@import './components/icon.css' layer(components);
@import './components/image.css' layer(components);
@import './components/repeatable.css' layer(components);
@import './components/text.css' layer(components);
@@ -0,0 +1,32 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'ያነሰ :count አሳይ',
'expand_list' => 'ተጨማሪ :count አሳይ',
],
'more_list_items' => 'እና ተጨማሪ :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'ስም',
],
'value' => [
'label' => 'ዋጋ',
],
],
'placeholder' => 'ምንም ግበኣቶች የሉም',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'عرض :count أقل',
'expand_list' => 'عرض :count أكثر',
],
'more_list_items' => 'و :count إضافية',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'المفتاح',
],
'value' => [
'label' => 'القيمة',
],
],
'placeholder' => 'لا توجد مدخلات',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count az göstər',
'expand_list' => ':count daha çox göstər',
],
'more_list_items' => 'və :count daha çox',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Açar',
],
'value' => [
'label' => 'Dəyər',
],
],
'placeholder' => 'Giriş yoxdur',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Покажи :count по-малко',
'expand_list' => 'Покажи още :count',
],
'more_list_items' => 'и още :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ключ',
],
'value' => [
'label' => 'Стойност',
],
],
'placeholder' => 'Няма записи',
],
],
];
@@ -0,0 +1,238 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'অনুলিপি করুন',
],
'add' => [
'label' => ':label এ যোগ করুন',
],
'add_between' => [
'label' => 'প্রবেশ করান',
],
'delete' => [
'label' => 'মুছে ফেলুন',
],
'reorder' => [
'label' => 'সরান',
],
'move_down' => [
'label' => 'নিচে সরান',
],
'move_up' => [
'label' => 'উপরে সরান',
],
'collapse' => [
'label' => 'ছোট করুন',
],
'expand' => [
'label' => 'বড় করুন',
],
'collapse_all' => [
'label' => 'সব ছোট করুন',
],
'expand_all' => [
'label' => 'সব বড় করুন',
],
],
],
'checkbox_list' => [
'actions' => [
'select_all' => [
'label' => 'সব নির্বাচিত করুন',
],
'deselect_all' => [
'label' => 'সব অনির্বাচিত করুন',
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'সারি যোগ করুন',
],
'delete' => [
'label' => 'সারি মুছে ফেলুন',
],
'reorder' => [
'label' => 'সারি পুনর্বিন্যাস করুন',
],
],
'fields' => [
'key' => [
'label' => 'চাবি',
],
'value' => [
'label' => 'মান',
],
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => ':label এ যোগ করুন',
],
'delete' => [
'label' => 'মুছে ফেলুন',
],
'clone' => [
'label' => 'অনুলিপি করুন',
],
'reorder' => [
'label' => 'সরান',
],
'move_down' => [
'label' => 'নিচে সরান',
],
'move_up' => [
'label' => 'উপরে সরান',
],
'collapse' => [
'label' => 'ছোট করুন',
],
'expand' => [
'label' => 'বড় করুন',
],
'collapse_all' => [
'label' => 'সব ছোট করুন',
],
'expand_all' => [
'label' => 'সব বড় করুন',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'লিংক',
'unlink' => 'আনলিংক',
],
'label' => 'ইউআরএল',
'placeholder' => 'ইউআরএল দিন',
],
],
'tools' => [
'attach_files' => 'নথি যোগ করুন',
'blockquote' => 'ব্লককোট',
'bold' => 'বোল্ড',
'bullet_list' => 'বুলেট তালিকা',
'code_block' => 'কোড ব্লক',
'h1' => 'শিরোনাম',
'h2' => 'শিরোনাম',
'h3' => 'উপশিরোনাম',
'italic' => 'তির্যক',
'link' => 'লিংক',
'ordered_list' => 'সংখ্যাযুক্ত তালিকা',
'redo' => 'পরবর্তী',
'strike' => 'স্ট্রাইকথ্রু',
'undo' => 'পূর্বাবস্থা',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'তৈরি করুন',
'actions' => [
'create' => [
'label' => 'তৈরি করুন',
],
],
],
],
],
'boolean' => [
'true' => 'হ্যাঁ',
'false' => 'না',
],
'loading_message' => 'লোড হচ্ছে...',
'max_items_message' => 'মাত্র :count টা নির্বাচন করা যাবে।',
'no_search_results_message' => 'খুঁজে পাওয়া যায়নি।',
'placeholder' => 'নির্বাচন করুন',
'searching_message' => 'খুঁজুন...',
'search_prompt' => 'লিখুন...',
],
'tags_input' => [
'placeholder' => 'নতুন ট্যাগ',
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Prikaži :count manje',
'expand_list' => 'Prikaži :count više',
],
'more_list_items' => 'i :count još',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ključ',
],
'value' => [
'label' => 'Vrijednost',
],
],
'placeholder' => 'Nema unosa',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Mostrar-ne :count menys',
'expand_list' => 'Mostrar-ne :count més',
],
'more_list_items' => 'i :count més',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Clau',
],
'value' => [
'label' => 'Valor',
],
],
'placeholder' => 'No hi ha entrades',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Show :count less',
'expand_list' => 'Show :count more',
],
'more_list_items' => 'and :count more',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Key',
],
'value' => [
'label' => 'Value',
],
],
'placeholder' => 'No entries',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Zobrazit o :count méně',
'expand_list' => 'Zobrazit o :count více',
],
'more_list_items' => 'a 1 další|a :count další| a :count dalších',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Klíč',
],
'value' => [
'label' => 'Hodnota',
],
],
'placeholder' => 'Žádné záznamy',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Vis :count mindre',
'expand_list' => 'Vis :count flere',
],
'more_list_items' => 'og :count flere',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Nøgle',
],
'value' => [
'label' => 'Værdi',
],
],
'placeholder' => 'Ingen data',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Zeige :count weniger',
'expand_list' => 'Zeige :count weitere',
],
'more_list_items' => 'und :count mehr',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Schlüssel',
],
'value' => [
'label' => 'Wert',
],
],
'placeholder' => 'Keine Einträge',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Show :count less',
'expand_list' => 'Show :count more',
],
'more_list_items' => 'and :count more',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Key',
],
'value' => [
'label' => 'Value',
],
],
'placeholder' => 'No entries',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Mostrar :count menos',
'expand_list' => 'Mostrar :count más',
],
'more_list_items' => 'y :count más',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Clave',
],
'value' => [
'label' => 'Valor',
],
],
'placeholder' => 'No hay entradas',
],
],
];
@@ -0,0 +1,37 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'نمایش :count کمتر',
'expand_list' => 'نمایش :count بیشتر',
],
'more_list_items' => 'و :count مورد دیگر',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'کلید',
],
'value' => [
'label' => 'مقدار',
],
],
'placeholder' => 'بدون ورودی',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Näytä :count vähemmän',
'expand_list' => 'Näytä :count lisää',
],
'more_list_items' => 'ja :count lisää',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Avain',
],
'value' => [
'label' => 'Arvo',
],
],
'placeholder' => 'Ei tietueita',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Afficher :count de moins',
'expand_list' => 'Afficher :count de plus',
],
'more_list_items' => 'et :count de plus',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Clé',
],
'value' => [
'label' => 'Valeur',
],
],
'placeholder' => 'Aucune entrée',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'הצג פחות :count',
'expand_list' => 'הצג עוד :count',
],
'more_list_items' => 'ועוד :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'שדה',
],
'value' => [
'label' => 'ערך',
],
],
'placeholder' => 'אין רשומות',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Prikaži :count manje',
'expand_list' => 'Prikaži :count više',
],
'more_list_items' => 'and :count more',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ključ',
],
'value' => [
'label' => 'Vrijednost',
],
],
'placeholder' => 'Nema unosa',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count elemmel kevesebb mutatása',
'expand_list' => ':count elemmel több mutatása',
],
'more_list_items' => ':count és több',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Kulcs',
],
'value' => [
'label' => 'Érték',
],
],
'placeholder' => 'Nincs megjeleníthető elem',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Ցույց տալ :count-ից ավելի քիչ',
'expand_list' => 'Ցույց տալ :count-ից ավելին',
],
'more_list_items' => 'և :count-ից ավելին',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Բանալի',
],
'value' => [
'label' => 'Արժեք',
],
],
'placeholder' => 'Գրառումներ չկան',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Sembunyikan :count lainnya',
'expand_list' => 'Tampilkan :count lainnya',
],
'more_list_items' => 'dan :count lainnya',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Kunci',
],
'value' => [
'label' => 'Nilai',
],
],
'placeholder' => 'Tidak ada entri',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Mostra :count in meno',
'expand_list' => 'Mostra altri :count',
],
'more_list_items' => 'e altri :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Chiave',
],
'value' => [
'label' => 'Valore',
],
],
'placeholder' => 'Nessun elemento',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count件非表示',
'expand_list' => ':count件表示',
],
'more_list_items' => 'あと:count件あります',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'キー',
],
'value' => [
'label' => '値',
],
],
'placeholder' => 'エントリがありません',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'იხილეთ :count ნაკლები',
'expand_list' => 'იხილეთ :count მეტი',
],
'more_list_items' => 'და კიდევ :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'იდენტიფიკატორი',
],
'value' => [
'label' => 'მნიშვნელობა',
],
],
'placeholder' => 'ჩანაწერები არ არის',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'បង្ហាញ :count តិច',
'expand_list' => 'បង្ហាញ :count ច្រើនទៀត',
],
'more_list_items' => 'និង :count ច្រើនទៀត',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'សោ',
],
'value' => [
'label' => 'តម្លៃ',
],
],
'placeholder' => 'គ្មានទិន្ន័យទេ',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count 개 더 접기',
'expand_list' => ':count 개 더 펼치기',
],
'more_list_items' => '그리고 :count 개 더',
],
'key_value' => [
'columns' => [
'key' => [
'label' => '키',
],
'value' => [
'label' => '값',
],
],
'placeholder' => '항목 없음',
],
],
];
@@ -0,0 +1,9 @@
<?php
return [
'text_entry' => [
'more_list_items' => 'وە :count ی زیاتر',
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Rodyti dar :count',
'expand_list' => 'Slėpti :count',
],
'more_list_items' => 'ir dar :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Raktas',
],
'value' => [
'label' => 'Reikšmė',
],
],
'placeholder' => 'Nėra įrašų',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count in tilang tlem rawh',
'expand_list' => ':count in tilang tam rawh',
],
'more_list_items' => 'and :count more',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Key',
],
'value' => [
'label' => 'Value',
],
],
'placeholder' => 'No entries',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count ခုကို ဖျောက်ထားသည်',
'expand_list' => ':count ခုကို ပြသထားသည်',
],
'more_list_items' => 'နောက်ထပ် :count ခု ကျန်ရှိသည်',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Key',
],
'value' => [
'label' => 'တန်ဖိုး',
],
],
'placeholder' => 'မှတ်တမ်းမရှိသေးပါ',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Vis :count mindre',
'expand_list' => 'Vis :count til',
],
'more_list_items' => 'og :count til',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Nøkkel',
],
'value' => [
'label' => 'Verdi',
],
],
'placeholder' => 'Ingen oppføringer',
],
],
];
@@ -0,0 +1,9 @@
<?php
return [
'text_entry' => [
'more_list_items' => 'र :count थप वस्तुहरू छन्',
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count minder tonen',
'expand_list' => ':count meer tonen',
],
'more_list_items' => 'en :count meer',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Sleutel',
],
'value' => [
'label' => 'Waarde',
],
],
'placeholder' => 'Geen items',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Pokaż :count mniej',
'expand_list' => 'Pokaż :count więcej',
],
'more_list_items' => 'dodaj :count więcej',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Klucz',
],
'value' => [
'label' => 'Wartość',
],
],
'placeholder' => 'Brak wpisów',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Mostrar menos :count',
'expand_list' => 'Mostrar mais :count',
],
'more_list_items' => 'e mais :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Chave',
],
'value' => [
'label' => 'Valor',
],
],
'placeholder' => 'Sem dados',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Mostrar menos :count',
'expand_list' => 'Mostrar mais :count',
],
'more_list_items' => 'e mais :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Chave',
],
'value' => [
'label' => 'Valor',
],
],
'placeholder' => 'Sem dados',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Скрыть :count',
'expand_list' => 'Показать еще :count',
],
'more_list_items' => 'и еще :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ключ',
],
'value' => [
'label' => 'Значение',
],
],
'placeholder' => 'Нет записей',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Zobraziť o :count menej',
'expand_list' => 'Zobraziť o :count viac',
],
'more_list_items' => 'a o :count viac',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Kľúč',
],
'value' => [
'label' => 'Hodnota',
],
],
'placeholder' => 'Žiadne záznamy',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Прикажи :count мање',
'expand_list' => 'Прикажи :count више',
],
'more_list_items' => 'и :count више',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Кључ',
],
'value' => [
'label' => 'Вредност',
],
],
'placeholder' => 'Без уноса',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Prikaži :count manje',
'expand_list' => 'Prikaži :count više',
],
'more_list_items' => 'i :count više',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ključ',
],
'value' => [
'label' => 'Vrednost',
],
],
'placeholder' => 'Bez unosa',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Visa :count färre',
'expand_list' => 'Visa :count till',
],
'more_list_items' => 'och :count till',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Namn',
],
'value' => [
'label' => 'Värde',
],
],
'placeholder' => 'Inga objekt',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'แสดงให้น้อยกว่านี้ :count รายการ',
'expand_list' => 'แสดงอีก :count รายการ',
],
'more_list_items' => 'และอีก :count รายการ',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'คีย์',
],
'value' => [
'label' => 'ค่า',
],
],
'placeholder' => 'ไม่มีรายการ',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count kayıt az göster',
'expand_list' => ':count kayıt daha göster',
],
'more_list_items' => 've :count kayıt daha',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Anahtar',
],
'value' => [
'label' => 'Değer',
],
],
'placeholder' => 'Kayıt yok',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Сховати :count',
'expand_list' => 'Показати ще :count',
],
'more_list_items' => 'ще :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Ключ',
],
'value' => [
'label' => 'Значення',
],
],
'placeholder' => 'Немає записів',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':count کم دکھائیں',
'expand_list' => ':count مزید دکھائیں',
],
'more_list_items' => 'اور :count مزید',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'کلید',
],
'value' => [
'label' => 'قدر',
],
],
'placeholder' => 'کوئی اندراج نہیں',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => ':countta kam ko\'rsatish',
'expand_list' => 'Yana :counttasini k\'rsatish',
],
'more_list_items' => 'va yana :countta',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Kalit so\'zi',
],
'value' => [
'label' => 'Qiymati',
],
],
'placeholder' => 'Ma\'lumot yo\'q',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => 'Hiển thị giảm :count',
'expand_list' => 'Hiển thị thêm :count',
],
'more_list_items' => 'và thêm :count',
],
'key_value' => [
'columns' => [
'key' => [
'label' => 'Khóa',
],
'value' => [
'label' => 'Giá trị',
],
],
'placeholder' => 'Không có mục nào',
],
],
];
@@ -0,0 +1,9 @@
<?php
return [
'text_entry' => [
'more_list_items' => '还有 :count 条记录',
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => '少顯示 :count 筆',
'expand_list' => '多顯示 :count 筆',
],
'more_list_items' => '多 :count 筆',
],
'key_value' => [
'columns' => [
'key' => [
'label' => '索引',
],
'value' => [
'label' => '值',
],
],
'placeholder' => '無項目',
],
],
];
@@ -0,0 +1,38 @@
<?php
return [
'entries' => [
'text' => [
'actions' => [
'collapse_list' => '少顯示 :count 筆',
'expand_list' => '多顯示 :count 筆',
],
'more_list_items' => '多 :count 筆',
],
'key_value' => [
'columns' => [
'key' => [
'label' => '索引',
],
'value' => [
'label' => '值',
],
],
'placeholder' => '無項目',
],
],
];
@@ -0,0 +1,132 @@
@props([
'alignment' => null,
'entry' => null,
'hasInlineLabel' => null,
'label' => null,
'labelSrOnly' => null,
])
@php
use Filament\Support\Enums\Alignment;
use Illuminate\View\ComponentAttributeBag;
if ($entry) {
$action ??= $entry->getAction();
$alignment ??= $entry->getAlignment();
$hasInlineLabel ??= $entry->hasInlineLabel();
$label ??= $entry->getLabel();
$labelSrOnly ??= $entry->isLabelHidden();
$url ??= $entry->getUrl();
$shouldOpenUrlInNewTab ??= $entry->shouldOpenUrlInNewTab();
}
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
$beforeLabelContainer = $entry?->getChildSchema($entry::BEFORE_LABEL_SCHEMA_KEY)?->toHtmlString();
$afterLabelContainer = $entry?->getChildSchema($entry::AFTER_LABEL_SCHEMA_KEY)?->toHtmlString();
$beforeContentContainer = $entry?->getChildSchema($entry::BEFORE_CONTENT_SCHEMA_KEY)?->toHtmlString();
$afterContentContainer = $entry?->getChildSchema($entry::AFTER_CONTENT_SCHEMA_KEY)?->toHtmlString();
@endphp
<div
{{
$attributes
->merge($entry?->getExtraEntryWrapperAttributes() ?? [], escape: false)
->class([
'fi-in-entry',
'fi-in-entry-has-inline-label' => $hasInlineLabel,
])
}}
>
@if ($label && $labelSrOnly)
<dt class="fi-in-entry-label fi-sr-only">
{{ $label }}
</dt>
@endif
<div class="fi-in-entry-label-col">
{{ $entry?->getChildSchema($entry::ABOVE_LABEL_SCHEMA_KEY) }}
@if (($label && (! $labelSrOnly)) || $beforeLabelContainer || $afterLabelContainer)
<div
@class([
'fi-in-entry-label-ctn',
($label instanceof \Illuminate\View\ComponentSlot) ? $label->attributes->get('class') : null,
])
>
{{ $beforeLabelContainer }}
@if ($label && (! $labelSrOnly))
<dt
{{
(
($label instanceof \Illuminate\View\ComponentSlot)
? $label->attributes
: (new ComponentAttributeBag)
)
->class(['fi-in-entry-label'])
}}
>
{{ $label }}
</dt>
@endif
{{ $afterLabelContainer }}
</div>
@endif
{{ $entry?->getChildSchema($entry::BELOW_LABEL_SCHEMA_KEY) }}
</div>
<div class="fi-in-entry-content-col">
{{ $entry?->getChildSchema($entry::ABOVE_CONTENT_SCHEMA_KEY) }}
<dd class="fi-in-entry-content-ctn">
{{ $beforeContentContainer }}
@if (filled($url))
<a
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab) }}
@class([
'fi-in-entry-content',
(($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : '')),
])
>
{{ $slot }}
</a>
@elseif (filled($action))
@php
$wireClickAction = $action->getLivewireClickHandler();
@endphp
<button
type="button"
wire:click="{{ $wireClickAction }}"
wire:loading.attr="disabled"
wire:target="{{ $wireClickAction }}"
@class([
'fi-in-entry-content',
(($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : '')),
])
>
{{ $slot }}
</button>
@else
<div
@class([
'fi-in-entry-content',
(($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : '')),
])
>
{{ $slot }}
</div>
@endif
{{ $afterContentContainer }}
</dd>
{{ $entry?->getChildSchema($entry::BELOW_CONTENT_SCHEMA_KEY) }}
</div>
</div>
@@ -0,0 +1,210 @@
<?php
namespace Filament\Infolists\Commands\FileGenerators\Concerns;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\ImageEntry;
use Filament\Infolists\Components\TextEntry;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Nette\PhpGenerator\Literal;
trait CanGenerateModelInfolists
{
/**
* @param ?class-string<Model> $model
*/
public function generateInfolistMethodBody(?string $model = null): string
{
return <<<PHP
return \$schema
->columns([
{$this->outputInfolistComponents($model)}
]);
PHP;
}
/**
* @param ?class-string<Model> $model
* @param array<string> $exceptColumns
* @return array<string>
*/
public function getInfolistComponents(?string $model = null, array $exceptColumns = []): array
{
if (! $this->isGenerated()) {
return [];
}
if (blank($model)) {
return [];
}
if (! class_exists($model)) {
return [];
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
$components = [];
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
$type = $this->parseColumnType($column);
if (in_array($type['name'], [
'json',
])) {
continue;
}
$componentName = $column['name'];
if (in_array($componentName, $exceptColumns)) {
continue;
}
if (str($componentName)->endsWith([
'_token',
])) {
continue;
}
if (str($componentName)->contains([
'password',
])) {
continue;
}
$componentData = [];
if (str($componentName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($componentName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($componentName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$componentName = "{$guessedRelationshipName}.{$guessedRelationshipTitleColumnName}";
$componentData['label'] = [(string) str($guessedRelationshipName)
->kebab()
->replace(['-', '_'], ' ')
->ucfirst()];
}
} else {
$guessedRelationshipName = null;
}
if (in_array($componentName, [
'id',
'sku',
'uuid',
])) {
$componentData['label'] = [Str::upper($componentName)];
}
if ($componentName === 'email') {
$componentData['label'] = ['Email address'];
}
if ($type['name'] === 'boolean') {
$componentData['type'] = IconEntry::class;
$componentData['boolean'] = [];
} else {
$componentData['type'] = match (true) {
$componentName === 'image', str($componentName)->startsWith('image_'), str($componentName)->contains('_image_'), str($componentName)->endsWith('_image') => ImageEntry::class,
default => TextEntry::class,
};
if (($type['name'] === 'enum') || array_key_exists($componentName, $this->getEnumCasts($model))) {
$componentData['badge'] = [];
}
if ($type['name'] === 'date') {
$componentData['date'] = [];
}
if ($type['name'] === 'time') {
$componentData['time'] = [];
}
if (in_array($type['name'], [
'datetime',
'timestamp',
])) {
$componentData['dateTime'] = [];
}
if (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
]) && blank($guessedRelationshipName)) {
$componentData[(in_array($componentName, [
'cost',
'money',
'price',
]) || str($componentName)->endsWith([
'_cost',
'_price',
]) || $type['name'] === 'money') ? 'money' : 'numeric'] = [];
}
}
if (in_array($componentName, [
'deleted_at',
])) {
$componentData['visible'] = [new Literal('fn (' . class_basename($model) . ' $record): bool => $record->trashed()')];
$this->namespace->addUse($model);
} elseif ($column['nullable']) {
$componentData['placeholder'] = ['-'];
}
if (in_array($type['name'], [
'text',
])) {
$componentData['columnSpanFull'] = [];
}
$this->importUnlessPartial($componentData['type']);
$components[$componentName] = $componentData;
}
return array_map(
function (array $componentData, string $componentName): string {
$component = (string) new Literal("{$this->simplifyFqn($componentData['type'])}::make(?)", [$componentName]);
unset($componentData['type']);
foreach ($componentData as $methodName => $parameters) {
$component .= new Literal(PHP_EOL . " ->{$methodName}(...?:)", [$parameters]);
}
return "{$component},";
},
$components,
array_keys($components),
);
}
/**
* @param ?class-string<Model> $model
* @param array<string> $exceptColumns
*/
public function outputInfolistComponents(?string $model = null, array $exceptColumns = []): string
{
$columns = $this->getInfolistComponents($model, $exceptColumns);
if (empty($columns)) {
return '//';
}
return implode(PHP_EOL . ' ', $columns);
}
}
@@ -0,0 +1,66 @@
<?php
namespace Filament\Infolists\Commands\FileGenerators;
use Filament\Infolists\Components\Entry;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Property;
class EntryClassGenerator extends ClassGenerator
{
final public function __construct(
protected string $fqn,
protected string $view,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
return [
$this->getExtends(),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return Entry::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addViewPropertyToClass($class);
}
protected function addViewPropertyToClass(ClassType $class): void
{
$property = $class->addProperty('view', $this->getView())
->setProtected()
->setType('string');
$this->configureViewProperty($property);
}
protected function configureViewProperty(Property $property): void {}
public function getFqn(): string
{
return $this->fqn;
}
public function getView(): ?string
{
return $this->view;
}
}
@@ -0,0 +1,174 @@
<?php
namespace Filament\Infolists\Commands;
use Filament\Infolists\Commands\FileGenerators\EntryClassGenerator;
use Filament\Support\Commands\Concerns\CanAskForComponentLocation;
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Exceptions\FailureCommandOutput;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:filament-infolist-entry', aliases: [
'filament:entry',
'filament:infolist-entry',
'infolists:entry',
'infolists:make-entry',
'make:filament-entry',
'make:infolist-entry',
])]
class MakeEntryCommand extends Command
{
use CanAskForComponentLocation;
use CanAskForViewLocation;
use CanManipulateFiles;
protected $description = 'Create a new infolist entry class and view';
protected $name = 'make:filament-infolist-entry';
protected string $fqnEnd;
protected string $fqn;
protected string $path;
protected string $view;
protected string $viewPath;
/**
* @var array<string>
*/
protected $aliases = [
'filament:entry',
'filament:infolist-entry',
'infolists:entry',
'infolists:make-entry',
'make:filament-entry',
'make:infolist-entry',
];
/**
* @return array<InputArgument>
*/
protected function getArguments(): array
{
return [
new InputArgument(
name: 'name',
mode: InputArgument::OPTIONAL,
description: 'The name of the entry to generate, optionally prefixed with directories',
),
];
}
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'force',
shortcut: 'F',
mode: InputOption::VALUE_NONE,
description: 'Overwrite the contents of the files if they already exist',
),
];
}
public function handle(): int
{
try {
$this->configureFqnEnd();
$this->configureLocation();
$this->createEntry();
$this->createView();
} catch (FailureCommandOutput) {
return static::FAILURE;
}
$this->components->info("Filament infolist entry [{$this->fqn}] created successfully.");
return static::SUCCESS;
}
protected function configureFqnEnd(): void
{
$this->fqnEnd = (string) str($this->argument('name') ?? text(
label: 'What is the entry name?',
placeholder: 'StatusSwitcherEntry',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->studly()
->replace('/', '\\');
}
protected function configureLocation(): void
{
[
$namespace,
$path,
$viewNamespace,
] = $this->askForComponentLocation(
path: 'Infolists/Components',
question: 'Where would you like to create the entry?',
);
$this->fqn = "{$namespace}\\{$this->fqnEnd}";
$this->path = (string) str("{$path}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
[
$this->view,
$this->viewPath,
] = $this->askForViewLocation(
str($this->fqn)
->afterLast('\\Infolists\\Components\\')
->prepend('Filament\\Infolists\\Components\\')
->replace('\\', '/')
->explode('/')
->map(Str::kebab(...))
->implode('.'),
defaultNamespace: $viewNamespace,
);
}
protected function createEntry(): void
{
if (! $this->option('force') && $this->checkForCollision($this->path)) {
throw new FailureCommandOutput;
}
$this->writeFile($this->path, app(EntryClassGenerator::class, [
'fqn' => $this->fqn,
'view' => $this->view,
]));
}
protected function createView(): void
{
if (blank($this->view)) {
return;
}
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
throw new FailureCommandOutput;
}
$this->copyStubToApp('EntryView', $this->viewPath);
}
}
@@ -0,0 +1,161 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanBeCopied;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Collection;
use Illuminate\Support\Js;
use Phiki\Grammar\Grammar;
use Phiki\Phiki;
use Phiki\Theme\Theme;
class CodeEntry extends Entry implements HasEmbeddedView
{
use CanBeCopied;
protected string | Grammar | Closure | null $grammar = null;
protected string | Theme | Closure | null $lightTheme = null;
protected string | Theme | Closure | null $darkTheme = null;
public function grammar(string | Grammar | Closure | null $grammar): static
{
$this->grammar = $grammar;
return $this;
}
public function getGrammar(): string | Grammar | null
{
return $this->evaluate($this->grammar);
}
public function lightTheme(string | Theme | Closure | null $theme): static
{
$this->lightTheme = $theme;
return $this;
}
public function getLightTheme(): string | Theme | null
{
return $this->evaluate($this->lightTheme);
}
public function darkTheme(string | Theme | Closure | null $theme): static
{
$this->darkTheme = $theme;
return $this;
}
public function getDarkTheme(): string | Theme | null
{
return $this->evaluate($this->darkTheme);
}
public function toEmbeddedHtml(): string
{
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-code',
]);
if (blank($state)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$phiki = new Phiki;
$grammar = $this->getGrammar();
$lightTheme = $this->getLightTheme();
$darkTheme = $this->getDarkTheme();
if (is_array($state)) {
$state = json_encode($state, flags: JSON_PRETTY_PRINT);
$grammar ??= Grammar::Json;
}
$grammar ??= Grammar::Html;
$lightTheme ??= Theme::GithubLight;
$darkTheme ??= Theme::GithubDarkHighContrast;
$isCopyable = $this->isCopyable($state);
$copyableStateJs = $isCopyable
? Js::from($this->getCopyableState($state) ?? $state)
: null;
$copyMessageJs = $isCopyable
? Js::from($this->getCopyMessage($state))
: null;
$copyMessageDurationJs = $isCopyable
? Js::from($this->getCopyMessageDuration($state))
: null;
$attributes = $attributes
->merge([
'x-on:click' => $isCopyable
? <<<JS
window.navigator.clipboard.writeText({$copyableStateJs})
\$tooltip({$copyMessageJs}, {
theme: \$store.theme,
timeout: {$copyMessageDurationJs},
})
JS
: null,
'x-tooltip' => filled($tooltip = $this->getTooltip($state))
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false)
->class([
'fi-copyable' => $isCopyable,
]);
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?= (string) $phiki->codeToHtml($state, $grammar, [
'light' => $lightTheme,
'dark' => $darkTheme,
]) ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
}
@@ -0,0 +1,125 @@
<?php
namespace Filament\Infolists\Components;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanBeCopied;
use Filament\Support\Concerns\CanWrap;
use Filament\Support\Enums\Alignment;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Js;
use Illuminate\View\ComponentAttributeBag;
class ColorEntry extends Entry implements HasEmbeddedView
{
use CanBeCopied;
use CanWrap;
public function toEmbeddedHtml(): string
{
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-color',
]);
if (blank($state)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$state = Arr::wrap($state);
$alignment = $this->getAlignment();
$attributes = $attributes
->class([
'fi-wrapped' => $this->canWrap(),
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
]);
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php foreach ($state as $stateItem) { ?>
<?php
$isCopyable = $this->isCopyable($stateItem);
$copyableStateJs = $isCopyable
? Js::from($this->getCopyableState($stateItem) ?? $stateItem)
: null;
$copyMessageJs = $isCopyable
? Js::from($this->getCopyMessage($stateItem))
: null;
$copyMessageDurationJs = $isCopyable
? Js::from($this->getCopyMessageDuration($stateItem))
: null;
?>
<div <?= (new ComponentAttributeBag)
->merge([
'x-on:click' => $isCopyable
? <<<JS
window.navigator.clipboard.writeText({$copyableStateJs})
\$tooltip({$copyMessageJs}, {
theme: \$store.theme,
timeout: {$copyMessageDurationJs},
})
JS
: null,
'x-tooltip' => filled($tooltip = $this->getTooltip($stateItem))
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false)
->class([
'fi-in-color-item',
'fi-copyable' => $isCopyable,
])
->style([
'background-color: ' . e($stateItem) => $stateItem,
])
->toHtml() ?>></div>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
public function canWrapByDefault(): bool
{
return true;
}
}
@@ -0,0 +1,500 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use BackedEnum;
use Closure;
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Concerns\CanConfigureCommonMark;
use Filament\Support\Contracts\HasLabel as LabelInterface;
use Filament\Support\Enums\ArgumentValue;
use Filament\Support\Facades\FilamentTimezone;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Carbon;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
trait CanFormatState
{
use CanConfigureCommonMark;
protected ?Closure $formatStateUsing = null;
protected int | Closure | null $characterLimit = null;
protected string | Closure | null $characterLimitEnd = null;
protected int | Closure | null $wordLimit = null;
protected string | Closure | null $wordLimitEnd = null;
protected string | Htmlable | Closure | null $prefix = null;
protected string | Htmlable | Closure | null $suffix = null;
protected string | Closure | null $timezone = null;
protected bool | Closure $isHtml = false;
protected bool | Closure $isMarkdown = false;
protected bool $isDate = false;
protected bool $isDateTime = false;
protected bool $isMoney = false;
protected bool $isNumeric = false;
protected bool $isTime = false;
public function markdown(bool | Closure $condition = true): static
{
$this->isMarkdown = $condition;
return $this;
}
public function date(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isDate = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($format, $timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->translatedFormat($component->evaluate($format) ?? $component->getContainer()->getDefaultDateDisplayFormat());
});
return $this;
}
public function dateTime(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isDateTime = true;
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultDateTimeDisplayFormat();
$this->date($format, $timezone);
return $this;
}
public function isoDate(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isDate = true;
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoDateDisplayFormat();
$this->formatStateUsing(static function (TextEntry $component, $state) use ($format, $timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->isoFormat($component->evaluate($format) ?? $component->getContainer()->getDefaultIsoDateDisplayFormat());
});
return $this;
}
public function isoDateTime(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isDateTime = true;
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoDateTimeDisplayFormat();
$this->isoDate($format, $timezone);
return $this;
}
public function since(string | Closure | null $timezone = null): static
{
$this->isDateTime = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->diffForHumans();
});
return $this;
}
public function dateTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->tooltip(static function (TextEntry $component, mixed $state) use ($format, $timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->translatedFormat($component->evaluate($format) ?? $component->getContainer()->getDefaultDateDisplayFormat());
});
return $this;
}
public function dateTimeTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultDateTimeDisplayFormat();
$this->dateTooltip($format, $timezone);
return $this;
}
public function timeTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultTimeDisplayFormat();
$this->dateTooltip($format, $timezone);
return $this;
}
public function sinceTooltip(string | Closure | null $timezone = null): static
{
$this->tooltip(static function (TextEntry $component, mixed $state) use ($timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->diffForHumans();
});
return $this;
}
public function isoDateTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoDateDisplayFormat();
$this->tooltip(static function (TextEntry $component, mixed $state) use ($format, $timezone): ?string {
if (blank($state)) {
return null;
}
return Carbon::parse($state)
->setTimezone($component->evaluate($timezone) ?? $component->getTimezone())
->isoFormat($component->evaluate($format) ?? $component->getContainer()->getDefaultIsoDateDisplayFormat());
});
return $this;
}
public function isoDateTimeTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoDateTimeDisplayFormat();
$this->isoDateTooltip($format, $timezone);
return $this;
}
public function isoTimeTooltip(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoTimeDisplayFormat();
$this->isoDateTooltip($format, $timezone);
return $this;
}
public function money(string | BackedEnum | Closure | null $currency = null, int | Closure $divideBy = 0, string | BackedEnum | Closure | null $locale = null, int | Closure | null $decimalPlaces = null): static
{
$this->isMoney = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($currency, $divideBy, $locale, $decimalPlaces): ?string {
if (blank($state)) {
return null;
}
if (! is_numeric($state)) {
return $state;
}
$currency = $component->evaluate($currency) ?? $component->getContainer()->getDefaultCurrency();
$locale = $component->evaluate($locale) ?? $component->getContainer()->getDefaultNumberLocale() ?? config('app.locale');
$decimalPlaces = $component->evaluate($decimalPlaces);
if ($divideBy = $component->evaluate($divideBy)) {
$state /= $divideBy;
}
if ($currency instanceof BackedEnum) {
$currency = (string) $currency->value;
}
if ($locale instanceof BackedEnum) {
$locale = (string) $locale->value;
}
return Number::currency($state, $currency, $locale, $decimalPlaces);
});
return $this;
}
public function numeric(int | Closure | null $decimalPlaces = null, string | Closure | null | ArgumentValue $decimalSeparator = ArgumentValue::Default, string | Closure | null | ArgumentValue $thousandsSeparator = ArgumentValue::Default, int | Closure | null $maxDecimalPlaces = null, string | Closure | null $locale = null): static
{
$this->isNumeric = true;
$this->formatStateUsing(static function (TextEntry $component, $state) use ($decimalPlaces, $decimalSeparator, $locale, $maxDecimalPlaces, $thousandsSeparator): ?string {
if (blank($state)) {
return null;
}
if (! is_numeric($state)) {
return $state;
}
$decimalPlaces = $component->evaluate($decimalPlaces);
$decimalSeparator = $component->evaluate($decimalSeparator);
$thousandsSeparator = $component->evaluate($thousandsSeparator);
if (
($decimalSeparator !== ArgumentValue::Default) ||
($thousandsSeparator !== ArgumentValue::Default)
) {
return number_format(
$state,
$decimalPlaces,
$decimalSeparator === ArgumentValue::Default ? '.' : $decimalSeparator,
$thousandsSeparator === ArgumentValue::Default ? ',' : $thousandsSeparator,
);
}
$locale = $component->evaluate($locale) ?? $component->getContainer()->getDefaultNumberLocale() ?? config('app.locale');
return Number::format($state, $decimalPlaces, $component->evaluate($maxDecimalPlaces), $locale);
});
return $this;
}
public function time(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isTime = true;
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultTimeDisplayFormat();
$this->date($format, $timezone);
return $this;
}
public function isoTime(string | Closure | null $format = null, string | Closure | null $timezone = null): static
{
$this->isTime = true;
$format ??= fn (TextEntry $component): string => $component->getContainer()->getDefaultIsoTimeDisplayFormat();
$this->isoDate($format, $timezone);
return $this;
}
public function timezone(string | Closure | null $timezone): static
{
$this->timezone = $timezone;
return $this;
}
public function limit(int | Closure | null $length = 100, string | Closure | null $end = '...'): static
{
$this->characterLimit = $length;
$this->characterLimitEnd = $end;
return $this;
}
public function words(int | Closure | null $words = 100, string | Closure | null $end = '...'): static
{
$this->wordLimit = $words;
$this->wordLimitEnd = $end;
return $this;
}
public function prefix(string | Htmlable | Closure | null $prefix): static
{
$this->prefix = $prefix;
return $this;
}
public function suffix(string | Htmlable | Closure | null $suffix): static
{
$this->suffix = $suffix;
return $this;
}
public function html(bool | Closure $condition = true): static
{
$this->isHtml = $condition;
return $this;
}
public function formatStateUsing(?Closure $callback): static
{
$this->formatStateUsing = $callback;
return $this;
}
public function formatState(mixed $state): mixed
{
$isHtml = $this->isHtml();
$state = $this->evaluate($this->formatStateUsing ?? $state, [
'state' => $state,
]);
if ($isHtml) {
if ($this->isMarkdown()) {
$state = Str::markdown($state, $this->getCommonMarkOptions(), $this->getCommonMarkExtensions());
}
$state = Str::sanitizeHtml($state);
}
if ($state instanceof Htmlable) {
$isHtml = true;
$state = $state->toHtml();
}
if ($state instanceof LabelInterface) {
$state = $state->getLabel();
}
if (! $isHtml) {
if ($characterLimit = $this->getCharacterLimit()) {
$state = Str::limit($state, $characterLimit, $this->getCharacterLimitEnd());
}
if ($wordLimit = $this->getWordLimit()) {
$state = Str::words($state, $wordLimit, $this->getWordLimitEnd());
}
}
$prefix = $this->getPrefix();
$suffix = $this->getSuffix();
if (
(($prefix instanceof Htmlable) || ($suffix instanceof Htmlable)) &&
(! $isHtml)
) {
$isHtml = true;
$state = e($state);
}
if (filled($prefix)) {
if ($prefix instanceof Htmlable) {
$prefix = $prefix->toHtml();
} elseif ($isHtml) {
$prefix = e($prefix);
}
$state = $prefix . $state;
}
if (filled($suffix)) {
if ($suffix instanceof Htmlable) {
$suffix = $suffix->toHtml();
} elseif ($isHtml) {
$suffix = e($suffix);
}
$state .= $suffix;
}
return $isHtml ? new HtmlString($state) : $state;
}
public function getCharacterLimit(): ?int
{
return $this->evaluate($this->characterLimit);
}
public function getCharacterLimitEnd(): ?string
{
return $this->evaluate($this->characterLimitEnd);
}
public function getWordLimit(): ?int
{
return $this->evaluate($this->wordLimit);
}
public function getWordLimitEnd(): ?string
{
return $this->evaluate($this->wordLimitEnd);
}
public function getTimezone(): string
{
return $this->evaluate($this->timezone) ?? ($this->isDateTime() ? FilamentTimezone::get() : config('app.timezone'));
}
public function isHtml(): bool
{
return $this->evaluate($this->isHtml) || $this->isMarkdown() || $this->isProse();
}
public function getPrefix(): string | Htmlable | null
{
return $this->evaluate($this->prefix);
}
public function getSuffix(): string | Htmlable | null
{
return $this->evaluate($this->suffix);
}
public function isMarkdown(): bool
{
return (bool) $this->evaluate($this->isMarkdown);
}
public function isDate(): bool
{
return $this->isDate;
}
public function isDateTime(): bool
{
return $this->isDateTime;
}
public function isMoney(): bool
{
return $this->isMoney;
}
public function isNumeric(): bool
{
return $this->isNumeric;
}
public function isTime(): bool
{
return $this->isTime;
}
}
@@ -0,0 +1,127 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
use Filament\Actions\Action;
use Filament\Support\Enums\Size;
use Illuminate\Support\Arr;
trait HasAffixes
{
/**
* @var array<Action> | null
*/
protected ?array $cachedSuffixActions = null;
/**
* @var array<Action | Closure>
*/
protected array $suffixActions = [];
/**
* @var array<Action> | null
*/
protected ?array $cachedPrefixActions = null;
/**
* @var array<Action | Closure>
*/
protected array $prefixActions = [];
public function prefixAction(Action | Closure $action): static
{
$this->prefixActions([$action]);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function prefixActions(array $actions): static
{
$this->prefixActions = [
...$this->prefixActions,
...$actions,
];
return $this;
}
public function suffixAction(Action | Closure $action): static
{
$this->suffixActions([$action]);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function suffixActions(array $actions): static
{
$this->suffixActions = [
...$this->suffixActions,
...$actions,
];
return $this;
}
/**
* @return array<Action>
*/
public function getPrefixActions(): array
{
return $this->cachedPrefixActions ?? $this->cachePrefixActions();
}
/**
* @return array<Action>
*/
public function cachePrefixActions(): array
{
$this->cachedPrefixActions = [];
foreach ($this->prefixActions as $prefixAction) {
foreach (Arr::wrap($this->evaluate($prefixAction)) as $action) {
$this->cachedPrefixActions[$action->getName()] = $this->prepareAction(
$action
->defaultSize(Size::Small)
->defaultView(Action::ICON_BUTTON_VIEW),
);
}
}
return $this->cachedPrefixActions;
}
/**
* @return array<Action>
*/
public function getSuffixActions(): array
{
return $this->cachedSuffixActions ?? $this->cacheSuffixActions();
}
/**
* @return array<Action>
*/
public function cacheSuffixActions(): array
{
$this->cachedSuffixActions = [];
foreach ($this->suffixActions as $suffixAction) {
foreach (Arr::wrap($this->evaluate($suffixAction)) as $action) {
$this->cachedSuffixActions[$action->getName()] = $this->prepareAction(
$action
->defaultSize(Size::Small)
->defaultView(Action::ICON_BUTTON_VIEW),
);
}
}
return $this->cachedSuffixActions;
}
}
@@ -0,0 +1,75 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
use Filament\Schemas\Components\Component;
use Filament\Support\Contracts\HasColor as ColorInterface;
trait HasColor
{
/**
* @var string | array<string> | bool | Closure | null
*/
protected string | array | bool | Closure | null $color = null;
/**
* @param string | array<string> | bool | Closure | null $color
*/
public function color(string | array | bool | Closure | null $color): static
{
$this->color = $color;
return $this;
}
/**
* @param array<mixed> | Closure $colors
*/
public function colors(array | Closure $colors): static
{
$this->color(function (Component $component, $state) use ($colors) {
$colors = $component->evaluate($colors);
$color = null;
foreach ($colors as $conditionalColor => $condition) {
if (is_numeric($conditionalColor)) {
$color = $condition;
} elseif ($condition instanceof Closure && $component->evaluate($condition)) {
$color = $conditionalColor;
} elseif ($condition === $state) {
$color = $conditionalColor;
}
}
return $color;
});
return $this;
}
/**
* @return string | array<string> | null
*/
public function getColor(mixed $state): string | array | null
{
$color = $this->evaluate($this->color, [
'state' => $state,
]);
if ($color === false) {
return null;
}
if (filled($color)) {
return $color;
}
if (! $state instanceof ColorInterface) {
return null;
}
return $state->getColor();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
use Illuminate\View\ComponentAttributeBag;
trait HasExtraEntryWrapperAttributes
{
/**
* @var array<array<mixed> | Closure>
*/
protected array $extraEntryWrapperAttributes = [];
/**
* @param array<mixed> | Closure $attributes
*/
public function extraEntryWrapperAttributes(array | Closure $attributes, bool $merge = false): static
{
if ($merge) {
$this->extraEntryWrapperAttributes[] = $attributes;
} else {
$this->extraEntryWrapperAttributes = [$attributes];
}
return $this;
}
/**
* @return array<mixed>
*/
public function getExtraEntryWrapperAttributes(): array
{
$temporaryAttributeBag = new ComponentAttributeBag;
foreach ($this->extraEntryWrapperAttributes as $extraEntryWrapperAttributes) {
$temporaryAttributeBag = $temporaryAttributeBag->merge($this->evaluate($extraEntryWrapperAttributes), escape: false);
}
return $temporaryAttributeBag->getAttributes();
}
public function getExtraEntryWrapperAttributesBag(): ComponentAttributeBag
{
return new ComponentAttributeBag($this->getExtraEntryWrapperAttributes());
}
}
@@ -0,0 +1,26 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Text;
use Illuminate\Contracts\Support\Htmlable;
trait HasHelperText
{
public function helperText(string | Htmlable | Closure | null $text): static
{
$this->belowContent(function (Component $component) use ($text): ?Text {
$content = $component->evaluate($text);
if (blank($content)) {
return null;
}
return Text::make($content);
});
return $this;
}
}
@@ -0,0 +1,186 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use BackedEnum;
use Closure;
use Filament\Actions\Action;
use Filament\Infolists\Components\Entry;
use Filament\Schemas\Components\Icon;
use Filament\Schemas\Components\Text;
use Illuminate\Contracts\Support\Htmlable;
trait HasHint
{
protected string | Htmlable | Closure | null $hint = null;
/**
* @var array<Action | Closure>
*/
protected array $hintActions = [];
/**
* @var string | array<string> | Closure | null
*/
protected string | array | Closure | null $hintColor = null;
protected string | BackedEnum | Htmlable | Closure | null $hintIcon = null;
protected string | Closure | null $hintIconTooltip = null;
protected function setUpHint(): void
{
$this->afterLabel(function (Entry $component): array {
$components = [];
if ($component->hasHint()) {
$components[] = Text::make(static function (Text $component): string | Htmlable | null {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return $parentComponent->getHint();
})
->color(static function (Text $component): string | array | null {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return $parentComponent->getHintColor();
})
->visible(static function (Text $component): bool {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return filled($parentComponent->getHint());
});
}
if ($component->hasHintIcon()) {
$components[] = Icon::make(static function (Icon $component): string | BackedEnum | Htmlable | null {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return $parentComponent->getHintIcon();
})
->tooltip(static function (Icon $component): ?string {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return $parentComponent->getHintIconTooltip();
})
->visible(static function (Icon $component): bool {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return filled($parentComponent->getHintIcon());
})
->color(static function (Icon $component): string | array | null {
/** @var self $parentComponent */
$parentComponent = $component->getContainer()->getParentComponent();
return $parentComponent->getHintColor();
});
}
return [
...$components,
...$component->getHintActions(),
];
});
}
public function hint(string | Htmlable | Closure | null $hint): static
{
$this->hint = $hint;
return $this;
}
/**
* @param string | array<string> | Closure | null $color
*/
public function hintColor(string | array | Closure | null $color): static
{
$this->hintColor = $color;
return $this;
}
public function hintIcon(string | BackedEnum | Htmlable | Closure | null $icon, string | Closure | null $tooltip = null): static
{
$this->hintIcon = $icon;
$this->hintIconTooltip($tooltip);
return $this;
}
public function hintIconTooltip(string | Closure | null $tooltip): static
{
$this->hintIconTooltip = $tooltip;
return $this;
}
public function hintAction(Action | Closure $action): static
{
$this->hintActions([$action]);
return $this;
}
/**
* @param array<Action | Closure> $actions
*/
public function hintActions(array $actions): static
{
$this->hintActions = [
...$this->hintActions,
...$actions,
];
return $this;
}
public function hasHint(): bool
{
return filled($this->hint);
}
public function getHint(): string | Htmlable | null
{
return $this->evaluate($this->hint);
}
/**
* @return string | array<string> | null
*/
public function getHintColor(): string | array | null
{
return $this->evaluate($this->hintColor);
}
public function hasHintIcon(): bool
{
return filled($this->hintIcon);
}
public function getHintIcon(): string | BackedEnum | Htmlable | null
{
return $this->evaluate($this->hintIcon);
}
public function getHintIconTooltip(): ?string
{
return $this->evaluate($this->hintIconTooltip);
}
/**
* @return array<Action>
*/
public function getHintActions(): array
{
return array_filter(array_map(
fn (Action | Closure $hintAction): ?Action => $this->evaluate($hintAction),
$this->hintActions,
));
}
}
@@ -0,0 +1,93 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use BackedEnum;
use Closure;
use Filament\Schemas\Components\Component;
use Filament\Support\Contracts\HasIcon as IconInterface;
use Filament\Support\Enums\IconPosition;
use Illuminate\Contracts\Support\Htmlable;
trait HasIcon
{
protected string | BackedEnum | bool | Closure | null $icon = null;
protected IconPosition | string | Closure | null $iconPosition = null;
public function icon(string | BackedEnum | bool | Closure | null $icon): static
{
$this->icon = $icon;
return $this;
}
/**
* @param array<mixed> | Closure $icons
*/
public function icons(array | Closure $icons): static
{
$this->icon(function (Component $component, $state) use ($icons) {
$icons = $component->evaluate($icons);
$icon = null;
foreach ($icons as $conditionalIcon => $condition) {
if (is_numeric($conditionalIcon)) {
$icon = $condition;
} elseif ($condition instanceof Closure && $component->evaluate($condition)) {
$icon = $conditionalIcon;
} elseif ($condition === $state) {
$icon = $conditionalIcon;
}
}
return $icon;
});
return $this;
}
public function iconPosition(IconPosition | string | Closure | null $iconPosition): static
{
$this->iconPosition = $iconPosition;
return $this;
}
public function getIcon(mixed $state): string | BackedEnum | Htmlable | null
{
$icon = $this->evaluate($this->icon, [
'state' => $state,
]);
if ($icon === false) {
return null;
}
if (filled($icon)) {
return $icon;
}
if (! $state instanceof IconInterface) {
return null;
}
return $state->getIcon();
}
public function getIconPosition(): IconPosition | string
{
$position = $this->evaluate($this->iconPosition);
if ($position instanceof IconPosition) {
return $position;
}
if (blank($position)) {
return IconPosition::Before;
}
return IconPosition::tryFrom($position) ?? $position;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
trait HasIconColor
{
/**
* @var string | array<string> | Closure | null
*/
protected string | array | Closure | null $iconColor = null;
/**
* @param string | array<string> | Closure | null $color
*/
public function iconColor(string | array | Closure | null $color): static
{
$this->iconColor = $color;
return $this;
}
/**
* @return string | array<int | string, string | int> | null
*/
public function getIconColor(mixed $state): string | array | null
{
return $this->evaluate($this->iconColor, [
'state' => $state,
]);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Filament\Infolists\Components\Concerns;
use Closure;
use Illuminate\Contracts\Support\Htmlable;
trait HasTooltip
{
protected string | Htmlable | Closure | null $tooltip = null;
protected string | Htmlable | Closure | null $emptyTooltip = null;
public function tooltip(string | Htmlable | Closure | null $tooltip): static
{
$this->tooltip = $tooltip;
return $this;
}
public function getTooltip(mixed $state = null): string | Htmlable | null
{
return $this->evaluate($this->tooltip, [
'state' => $state,
]);
}
public function emptyTooltip(string | Htmlable | Closure | null $tooltip): static
{
$this->emptyTooltip = $tooltip;
return $this;
}
public function getEmptyTooltip(): string | Htmlable | null
{
return $this->evaluate($this->emptyTooltip);
}
}
@@ -0,0 +1,351 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Concerns\CanOpenUrl;
use Filament\Schemas\Components\Concerns\HasLabel;
use Filament\Schemas\Components\Concerns\HasName;
use Filament\Schemas\Schema;
use Filament\Support\Concerns\HasAlignment;
use Filament\Support\Concerns\HasPlaceholder;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\Size;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\View\ComponentAttributeBag;
use Illuminate\View\ComponentSlot;
use LogicException;
use function Filament\Support\generate_href_html;
class Entry extends Component
{
use CanOpenUrl;
use Concerns\HasExtraEntryWrapperAttributes;
use Concerns\HasHelperText;
use Concerns\HasHint;
use Concerns\HasTooltip;
use HasAlignment;
use HasLabel {
getLabel as getBaseLabel;
}
use HasName;
use HasPlaceholder;
protected string $viewIdentifier = 'entry';
const ABOVE_LABEL_SCHEMA_KEY = 'above_label';
const BELOW_LABEL_SCHEMA_KEY = 'below_label';
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 BEFORE_CONTENT_SCHEMA_KEY = 'before_content';
const AFTER_CONTENT_SCHEMA_KEY = 'after_content';
final public function __construct(string $name)
{
$this->name($name);
$this->statePath($name);
}
public static function make(?string $name = null): static
{
$entryClass = static::class;
$name ??= static::getDefaultName();
if (blank($name)) {
throw new LogicException("Entry of class [$entryClass] must have a unique name, passed to the [make()] method.");
}
$static = app($entryClass, ['name' => $name]);
$static->configure();
return $static;
}
protected function setUp(): void
{
parent::setUp();
$this->setUpHint();
}
public static function getDefaultName(): ?string
{
return null;
}
public function getState(): mixed
{
return $this->getConstantState();
}
public function getLabel(): string | Htmlable | null
{
if (filled($label = $this->getBaseLabel())) {
return $label;
}
$label = (string) str($this->getName())
->afterLast('.')
->kebab()
->replace(['-', '_'], ' ')
->ucfirst();
return $this->shouldTranslateLabel ? __($label) : $label;
}
public function state(mixed $state): static
{
$this->constantState($state);
return $this;
}
public function getStateUsing(mixed $callback): static
{
$this->state($callback);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function aboveLabel(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::ABOVE_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function belowLabel(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::BELOW_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function beforeLabel(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::BEFORE_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function afterLabel(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::AFTER_LABEL_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function aboveContent(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::ABOVE_CONTENT_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function belowContent(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::BELOW_CONTENT_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function beforeContent(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::BEFORE_CONTENT_SCHEMA_KEY);
return $this;
}
/**
* @param array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components
*/
public function afterContent(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components): static
{
$this->childComponents($components, static::AFTER_CONTENT_SCHEMA_KEY);
return $this;
}
protected function makeChildSchema(string $key): Schema
{
$schema = parent::makeChildSchema($key);
if (in_array($key, [static::AFTER_LABEL_SCHEMA_KEY, static::AFTER_CONTENT_SCHEMA_KEY])) {
$schema->alignEnd();
}
return $schema;
}
protected function configureChildSchema(Schema $schema, string $key): Schema
{
$schema = parent::configureChildSchema($schema, $key);
if (in_array($key, [
static::ABOVE_LABEL_SCHEMA_KEY,
static::BELOW_LABEL_SCHEMA_KEY,
static::BEFORE_LABEL_SCHEMA_KEY,
static::AFTER_LABEL_SCHEMA_KEY,
static::ABOVE_CONTENT_SCHEMA_KEY,
static::BELOW_CONTENT_SCHEMA_KEY,
static::BEFORE_CONTENT_SCHEMA_KEY,
static::AFTER_CONTENT_SCHEMA_KEY,
])) {
$schema
->inline()
->embeddedInParentComponent()
->modifyActionsUsing(fn (Action $action) => $action
->defaultSize(Size::Small)
->defaultView(Action::LINK_VIEW))
->modifyActionGroupsUsing(fn (ActionGroup $actionGroup) => $actionGroup->defaultSize(Size::Small));
}
return $schema;
}
public function wrapEmbeddedHtml(string $html): string
{
$view = $this->getEntryWrapperAbsoluteView();
if ($view !== 'filament-infolists::components.entry-wrapper') {
return view($this->getEntryWrapperAbsoluteView(), [
'entry' => $this,
'slot' => new ComponentSlot($html),
])->toHtml();
}
$hasInlineLabel = $this->hasInlineLabel();
$alignment = $this->getAlignment();
$label = $this->getLabel();
$labelSrOnly = $this->isLabelHidden();
$action = $this->getAction();
$url = $this->getUrl();
$wrapperTag = match (true) {
filled($url) => 'a',
filled($action) => 'button',
default => 'div',
};
if (! $alignment instanceof Alignment) {
$alignment = filled($alignment) ? (Alignment::tryFrom($alignment) ?? $alignment) : null;
}
$aboveLabelSchema = $this->getChildSchema($this::ABOVE_LABEL_SCHEMA_KEY)?->toHtmlString();
$belowLabelSchema = $this->getChildSchema($this::BELOW_LABEL_SCHEMA_KEY)?->toHtmlString();
$beforeLabelSchema = $this->getChildSchema($this::BEFORE_LABEL_SCHEMA_KEY)?->toHtmlString();
$afterLabelSchema = $this->getChildSchema($this::AFTER_LABEL_SCHEMA_KEY)?->toHtmlString();
$beforeContentSchema = $this->getChildSchema($this::BEFORE_CONTENT_SCHEMA_KEY)?->toHtmlString();
$afterContentSchema = $this->getChildSchema($this::AFTER_CONTENT_SCHEMA_KEY)?->toHtmlString();
$attributes = $this->getExtraEntryWrapperAttributesBag()
->class([
'fi-in-entry',
'fi-in-entry-has-inline-label' => $hasInlineLabel,
]);
$contentAttributes = (new ComponentAttributeBag)
->merge([
'type' => ($wrapperTag === 'button') ? 'button' : null,
'wire:click' => $wireClickAction = $action?->getLivewireClickHandler(),
'wire:loading.attr' => ($wrapperTag === 'button') ? 'disabled' : null,
'wire:target' => $wireClickAction,
], escape: false)
->class([
'fi-in-entry-content',
(($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : '')),
]);
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($label) && $labelSrOnly) { ?>
<dt class="fi-in-entry-label fi-hidden">
<?= e($label) ?>
</dt>
<?php } ?>
<?php if ((filled($label) && (! $labelSrOnly)) || $hasInlineLabel || $aboveLabelSchema || $belowLabelSchema || $beforeLabelSchema || $afterLabelSchema) { ?>
<div class="fi-in-entry-label-col">
<?= $aboveLabelSchema?->toHtml() ?>
<?php if ((filled($label) && (! $labelSrOnly)) || $beforeLabelSchema || $afterLabelSchema) { ?>
<div class="fi-in-entry-label-ctn">
<?= $beforeLabelSchema?->toHtml() ?>
<?php if (filled($label) && (! $labelSrOnly)) { ?>
<dt class="fi-in-entry-label">
<?= e($label) ?>
</dt>
<?php } ?>
<?= $afterLabelSchema?->toHtml() ?>
</div>
<?php } ?>
<?= $belowLabelSchema?->toHtml() ?>
</div>
<?php } ?>
<div class="fi-in-entry-content-col">
<?= $this->getChildSchema($this::ABOVE_CONTENT_SCHEMA_KEY)?->toHtml() ?>
<dd class="fi-in-entry-content-ctn">
<?= $beforeContentSchema?->toHtml() ?>
<<?= $wrapperTag ?> <?php if ($wrapperTag === 'a') {
echo generate_href_html($url, $this->shouldOpenUrlInNewTab())->toHtml();
} ?> <?= $contentAttributes->toHtml() ?>>
<?= $html ?>
</<?= $wrapperTag ?>>
<?= $afterContentSchema?->toHtml() ?>
</dd>
<?= $this->getChildSchema($this::BELOW_CONTENT_SCHEMA_KEY)?->toHtml() ?>
</div>
</div>
<?php return ob_get_clean();
}
public function isDehydrated(): bool
{
return false;
}
}
@@ -0,0 +1,322 @@
<?php
namespace Filament\Infolists\Components;
use BackedEnum;
use Closure;
use Filament\Infolists\View\Components\IconEntryComponent\IconComponent;
use Filament\Infolists\View\InfolistsIconAlias;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanWrap;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Js;
use Illuminate\View\ComponentAttributeBag;
use function Filament\Support\generate_icon_html;
class IconEntry extends Entry implements HasEmbeddedView
{
use CanWrap;
use Concerns\HasColor {
getColor as getBaseColor;
}
use Concerns\HasIcon {
getIcon as getBaseIcon;
}
protected bool | Closure | null $isBoolean = null;
/**
* @var string | array<string> | Closure | null
*/
protected string | array | Closure | null $falseColor = null;
protected string | BackedEnum | Htmlable | Closure | false | null $falseIcon = null;
/**
* @var string | array<string> | Closure | null
*/
protected string | array | Closure | null $trueColor = null;
protected string | BackedEnum | Htmlable | Closure | false | null $trueIcon = null;
protected IconSize | string | Closure | null $size = null;
protected bool | Closure $isListWithLineBreaks = false;
public function boolean(bool | Closure $condition = true): static
{
$this->isBoolean = $condition;
return $this;
}
/**
* @param string | array<int | string, string | int> | Closure | null $color
*/
public function false(string | BackedEnum | Htmlable | Closure | false | null $icon = null, string | array | Closure | null $color = null): static
{
$this->falseIcon($icon);
$this->falseColor($color);
return $this;
}
/**
* @param string | array<string> | Closure | null $color
*/
public function falseColor(string | array | Closure | null $color): static
{
$this->boolean();
$this->falseColor = $color;
return $this;
}
public function falseIcon(string | BackedEnum | Htmlable | Closure | false | null $icon): static
{
$this->boolean();
$this->falseIcon = $icon;
return $this;
}
/**
* @param string | array<int | string, string | int> | Closure | null $color
*/
public function true(string | BackedEnum | Htmlable | Closure | false | null $icon = null, string | array | Closure | null $color = null): static
{
$this->trueIcon($icon);
$this->trueColor($color);
return $this;
}
/**
* @param string | array<string> | Closure | null $color
*/
public function trueColor(string | array | Closure | null $color): static
{
$this->boolean();
$this->trueColor = $color;
return $this;
}
public function trueIcon(string | BackedEnum | Htmlable | Closure | false | null $icon): static
{
$this->boolean();
$this->trueIcon = $icon;
return $this;
}
public function size(IconSize | string | Closure | null $size): static
{
$this->size = $size;
return $this;
}
public function getSize(mixed $state): IconSize | string | null
{
return $this->evaluate($this->size, [
'state' => $state,
]);
}
public function getIcon(mixed $state): string | BackedEnum | Htmlable | null
{
if (filled($icon = $this->getBaseIcon($state))) {
return $icon;
}
if (! $this->isBoolean()) {
return null;
}
if ($state === null) {
return null;
}
return $state ? $this->getTrueIcon() : $this->getFalseIcon();
}
/**
* @return string | array<int | string, string | int> | null
*/
public function getColor(mixed $state): string | array | null
{
if (filled($color = $this->getBaseColor($state))) {
return $color;
}
if (! $this->isBoolean()) {
return null;
}
if ($state === null) {
return null;
}
return $state ? $this->getTrueColor() : $this->getFalseColor();
}
/**
* @return string | array<string>
*/
public function getFalseColor(): string | array
{
return $this->evaluate($this->falseColor) ?? 'danger';
}
public function getFalseIcon(): string | BackedEnum | Htmlable | null
{
$icon = $this->evaluate($this->falseIcon);
if ($icon === false) {
return null;
}
return $icon
?? FilamentIcon::resolve(InfolistsIconAlias::COMPONENTS_ICON_ENTRY_FALSE)
?? Heroicon::OutlinedXCircle;
}
/**
* @return string | array<string>
*/
public function getTrueColor(): string | array
{
return $this->evaluate($this->trueColor) ?? 'success';
}
public function getTrueIcon(): string | BackedEnum | Htmlable | null
{
$icon = $this->evaluate($this->trueIcon);
if ($icon === false) {
return null;
}
return $icon
?? FilamentIcon::resolve(InfolistsIconAlias::COMPONENTS_ICON_ENTRY_TRUE)
?? Heroicon::OutlinedCheckCircle;
}
public function listWithLineBreaks(bool | Closure $condition = true): static
{
$this->isListWithLineBreaks = $condition;
return $this;
}
public function isListWithLineBreaks(): bool
{
return (bool) $this->evaluate($this->isListWithLineBreaks);
}
public function isBoolean(): bool
{
if (blank($this->isBoolean)) {
$this->isBoolean = $this->getRecord()?->hasCast($this->getName(), ['bool', 'boolean']);
}
return (bool) $this->evaluate($this->isBoolean);
}
public function toEmbeddedHtml(): string
{
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-icon',
]);
if (blank($state)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$state = Arr::wrap($state);
$alignment = $this->getAlignment();
$attributes = $attributes
->class([
'fi-in-icon-has-line-breaks' => $this->isListWithLineBreaks(),
'fi-wrapped' => $this->canWrap(),
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
]);
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php foreach ($state as $stateItem) { ?>
<?php
$icon = $this->getIcon($stateItem);
if (blank($icon)) {
continue;
}
$color = $this->getColor($stateItem);
$size = $this->getSize($stateItem);
?>
<?= generate_icon_html($icon, attributes: (new ComponentAttributeBag)
->merge([
'x-tooltip' => filled($tooltip = $this->getTooltip($stateItem))
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false)
->color(IconComponent::class, $color), size: $size ?? IconSize::Large)
->toHtml() ?>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
public function canWrapByDefault(): bool
{
return true;
}
}
@@ -0,0 +1,554 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanWrap;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\TextSize;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Js;
use Illuminate\View\ComponentAttributeBag;
use League\Flysystem\UnableToCheckFileExistence;
use Throwable;
use function Filament\Support\generate_href_html;
class ImageEntry extends Entry implements HasEmbeddedView
{
use CanWrap;
protected string | Closure | null $diskName = null;
protected int | string | Closure | null $imageHeight = null;
protected int | string | Closure | null $imageWidth = null;
protected bool | Closure $isCircular = false;
protected bool | Closure $isSquare = false;
protected string | Closure | null $visibility = null;
protected int | string | Closure | null $width = null;
/**
* @var array<mixed> | Closure
*/
protected array | Closure $extraImgAttributes = [];
protected string | Closure | null $defaultImageUrl = null;
protected bool | Closure $isStacked = false;
protected int | Closure | null $overlap = null;
protected int | Closure | null $ring = null;
protected int | Closure | null $limit = null;
protected bool | Closure $hasLimitedRemainingText = false;
protected bool | Closure $isLimitedRemainingTextSeparate = false;
protected TextSize | string | Closure | null $limitedRemainingTextSize = null;
protected bool | Closure $shouldCheckFileExistence = true;
public function disk(string | Closure | null $disk): static
{
$this->diskName = $disk;
return $this;
}
public function imageHeight(int | string | Closure | null $height): static
{
$this->imageHeight = $height;
return $this;
}
/**
* @deprecated Use `imageHeight()` instead.
*/
public function height(int | string | Closure | null $height): static
{
$this->imageHeight($height);
return $this;
}
public function imageSize(int | string | Closure $size): static
{
$this->imageWidth($size);
$this->imageHeight($size);
return $this;
}
/**
* @deprecated Use `imageSize()` instead.
*/
public function size(int | string | Closure $size): static
{
$this->imageSize($size);
return $this;
}
public function circular(bool | Closure $condition = true): static
{
$this->isCircular = $condition;
return $this;
}
public function square(bool | Closure $condition = true): static
{
$this->isSquare = $condition;
return $this;
}
public function visibility(string | Closure | null $visibility): static
{
$this->visibility = $visibility;
return $this;
}
public function imageWidth(int | string | Closure | null $width): static
{
$this->imageWidth = $width;
return $this;
}
/**
* @deprecated Use `imageWidth()` instead.
*/
public function width(int | string | Closure | null $width): static
{
$this->imageWidth($width);
return $this;
}
public function getDisk(): Filesystem
{
return Storage::disk($this->getDiskName());
}
public function getDiskName(): string
{
$name = $this->evaluate($this->diskName);
if (filled($name)) {
return $name;
}
$defaultName = config('filament.default_filesystem_disk');
if (
($defaultName === 'public')
&& ($this->getCustomVisibility() === 'private')
) {
return 'local';
}
return $defaultName;
}
public function getImageHeight(): ?string
{
$height = $this->evaluate($this->imageHeight);
if ($height === null) {
return null;
}
if (is_int($height)) {
return "{$height}px";
}
return $height;
}
/**
* @deprecated Use `getImageHeight()` instead.
*/
public function getHeight(): ?string
{
return $this->getImageHeight();
}
public function defaultImageUrl(string | Closure | null $url): static
{
$this->defaultImageUrl = $url;
return $this;
}
public function getImageUrl(?string $state = null): ?string
{
if ((filter_var($state, FILTER_VALIDATE_URL) !== false) || str($state)->startsWith('data:')) {
return $state;
}
/** @var FilesystemAdapter $storage */
$storage = $this->getDisk();
if ($this->shouldCheckFileExistence()) {
try {
if (! $storage->exists($state)) {
return null;
}
} catch (UnableToCheckFileExistence $exception) {
return null;
}
}
if ($this->getVisibility() === 'private') {
try {
return $storage->temporaryUrl(
$state,
now()->addMinutes(30)->endOfHour(),
);
} catch (Throwable $exception) {
// This driver does not support creating temporary URLs.
}
}
return $storage->url($state);
}
public function getDefaultImageUrl(): ?string
{
return $this->evaluate($this->defaultImageUrl);
}
public function getVisibility(): string
{
$visibility = $this->getCustomVisibility();
if (filled($visibility)) {
return $visibility;
}
return ($this->getDiskName() === 'public') ? 'public' : 'private';
}
public function getCustomVisibility(): ?string
{
return $this->evaluate($this->visibility);
}
public function getImageWidth(): ?string
{
$width = $this->evaluate($this->imageWidth);
if ($width === null) {
return null;
}
if (is_int($width)) {
return "{$width}px";
}
return $width;
}
/**
* @deprecated Use `getImageWidth()` instead.
*/
public function getWidth(): ?string
{
return $this->getImageWidth();
}
public function isCircular(): bool
{
return (bool) $this->evaluate($this->isCircular);
}
public function isSquare(): bool
{
return (bool) $this->evaluate($this->isSquare);
}
/**
* @param array<mixed> | Closure $attributes
*/
public function extraImgAttributes(array | Closure $attributes): static
{
$this->extraImgAttributes = $attributes;
return $this;
}
/**
* @return array<mixed>
*/
public function getExtraImgAttributes(): array
{
return $this->evaluate($this->extraImgAttributes);
}
public function getExtraImgAttributeBag(): ComponentAttributeBag
{
return new ComponentAttributeBag($this->getExtraImgAttributes());
}
public function stacked(bool | Closure $condition = true): static
{
$this->isStacked = $condition;
return $this;
}
public function isStacked(): bool
{
return (bool) $this->evaluate($this->isStacked);
}
public function overlap(int | Closure | null $overlap): static
{
$this->overlap = $overlap;
return $this;
}
public function getOverlap(): ?int
{
return $this->evaluate($this->overlap);
}
public function ring(string | Closure | null $ring): static
{
$this->ring = $ring;
return $this;
}
public function getRing(): ?int
{
return $this->evaluate($this->ring);
}
public function limit(int | Closure | null $limit = 3): static
{
$this->limit = $limit;
return $this;
}
public function getLimit(): ?int
{
return $this->evaluate($this->limit);
}
public function limitedRemainingText(bool | Closure $condition = true, bool | Closure $isSeparate = false, TextSize | string | Closure | null $size = null): static
{
$this->hasLimitedRemainingText = $condition;
$this->limitedRemainingTextSeparate($isSeparate);
$this->limitedRemainingTextSize($size);
return $this;
}
public function limitedRemainingTextSeparate(bool | Closure $condition = true): static
{
$this->isLimitedRemainingTextSeparate = $condition;
return $this;
}
public function hasLimitedRemainingText(): bool
{
return (bool) $this->evaluate($this->hasLimitedRemainingText);
}
public function isLimitedRemainingTextSeparate(): bool
{
return (bool) $this->evaluate($this->isLimitedRemainingTextSeparate);
}
public function limitedRemainingTextSize(TextSize | string | Closure | null $size): static
{
$this->limitedRemainingTextSize = $size;
return $this;
}
public function getLimitedRemainingTextSize(): TextSize | string | null
{
$size = $this->evaluate($this->limitedRemainingTextSize);
if (blank($size)) {
return null;
}
if (is_string($size)) {
$size = TextSize::tryFrom($size) ?? $size;
}
return $size;
}
public function checkFileExistence(bool | Closure $condition = true): static
{
$this->shouldCheckFileExistence = $condition;
return $this;
}
public function shouldCheckFileExistence(): bool
{
return (bool) $this->evaluate($this->shouldCheckFileExistence);
}
public function toEmbeddedHtml(): string
{
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-image',
]);
$defaultImageUrl = $this->getDefaultImageUrl();
if (blank($state) && filled($defaultImageUrl)) {
$state = [null];
}
if (blank($state)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$state = Arr::wrap($state);
$stateCount = count($state);
$limit = $this->getLimit() ?? $stateCount;
$stateOverLimitCount = ($limit && ($stateCount > $limit))
? ($stateCount - $limit)
: 0;
if ($stateOverLimitCount) {
$state = array_slice($state, 0, $limit);
}
$alignment = $this->getAlignment();
$isCircular = $this->isCircular();
$isSquare = $this->isSquare();
$isStacked = $this->isStacked();
$hasLimitedRemainingText = $stateOverLimitCount && $this->hasLimitedRemainingText();
$limitedRemainingTextSize = $this->getLimitedRemainingTextSize();
$height = $this->getImageHeight() ?? ($isStacked ? '2.5rem' : '8rem');
$width = $this->getImageWidth() ?? (($isCircular || $isSquare) ? $height : null);
$attributes = $attributes
->class([
'fi-circular' => $isCircular,
'fi-wrapped' => $this->canWrap(),
'fi-stacked' => $isStacked,
($isStacked && is_int($ring = $this->getRing())) ? "fi-in-image-ring fi-in-image-ring-{$ring}" : '',
($isStacked && ($overlap = ($this->getOverlap() ?? 2))) ? "fi-in-image-overlap-{$overlap}" : '',
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
]);
$shouldOpenUrlInNewTab = $this->shouldOpenUrlInNewTab();
$formatState = function (mixed $stateItem) use ($defaultImageUrl, $width, $height, $shouldOpenUrlInNewTab): string {
$item = '<img ' . $this->getExtraImgAttributeBag()
->merge([
'src' => filled($stateItem) ? ($this->getImageUrl($stateItem) ?? $defaultImageUrl) : $defaultImageUrl,
'x-tooltip' => filled($tooltip = $this->getTooltip($stateItem))
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false)
->style([
"height: {$height}" => $height,
"width: {$width}" => $width,
])
->toHtml()
. ' />';
if (filled($url = $this->getUrl($stateItem))) {
$item = '<a ' . generate_href_html($url, $shouldOpenUrlInNewTab)->toHtml() . '>' . $item . '</a>';
}
return $item;
};
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php foreach ($state as $stateItem) { ?>
<?= $formatState($stateItem) ?>
<?php } ?>
<?php if ($hasLimitedRemainingText) { ?>
<div <?= (new ComponentAttributeBag)
->class([
'fi-in-image-limited-remaining-text',
(($limitedRemainingTextSize instanceof TextSize) ? "fi-size-{$limitedRemainingTextSize->value}" : $limitedRemainingTextSize) => $limitedRemainingTextSize,
])
->style([
"height: {$height}" => $height,
"width: {$width}" => $width,
])
->toHtml() ?>>
+<?= $stateOverLimitCount ?>
</div>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
public function canWrapByDefault(): bool
{
return true;
}
}
@@ -0,0 +1,109 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Illuminate\Support\Collection;
class KeyValueEntry extends Entry implements HasEmbeddedView
{
protected string | Closure | null $keyLabel = null;
protected string | Closure | null $valueLabel = null;
protected function setUp(): void
{
parent::setUp();
$this->placeholder(__('filament-infolists::components.entries.key_value.placeholder'));
}
public function keyLabel(string | Closure | null $label): static
{
$this->keyLabel = $label;
return $this;
}
public function valueLabel(string | Closure | null $label): static
{
$this->valueLabel = $label;
return $this;
}
/**
* @deprecated Use `placeholder()` instead.
*/
public function emptyMessage(string | Closure | null $message): static
{
$this->placeholder($message);
return $this;
}
public function getKeyLabel(): string
{
return $this->evaluate($this->keyLabel) ?? __('filament-infolists::components.entries.key_value.columns.key.label');
}
public function getValueLabel(): string
{
return $this->evaluate($this->valueLabel) ?? __('filament-infolists::components.entries.key_value.columns.value.label');
}
public function toEmbeddedHtml(): string
{
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-key-value',
]);
ob_start(); ?>
<table <?= $attributes->toHtml() ?>>
<thead>
<tr>
<th scope="col">
<?= e($this->getKeyLabel()) ?>
</th>
<th scope="col">
<?= e($this->getValueLabel()) ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach (($state ?? []) as $key => $value) { ?>
<tr>
<td>
<?= e($key) ?>
</td>
<td>
<?= e($value) ?>
</td>
</tr>
<?php } ?>
<?php if (empty($state)) { ?>
<tr>
<td colspan="2" class="fi-in-placeholder">
<?= e($this->getPlaceholder()) ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
}
@@ -0,0 +1,245 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Exception;
use Filament\Infolists\Components\RepeatableEntry\TableColumn;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Concerns\HasContainerGridLayout;
use Filament\Schemas\Schema;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanBeContained;
use Filament\Support\Enums\Alignment;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Js;
class RepeatableEntry extends Entry implements HasEmbeddedView
{
use CanBeContained;
use HasContainerGridLayout;
/**
* @var array<TableColumn> | Closure | null
*/
protected array | Closure | null $tableColumns = null;
/**
* Configure table columns for display
*
* @param array<TableColumn> | Closure | null $columns
*/
public function table(array | Closure | null $columns): static
{
$this->tableColumns = $columns;
return $this;
}
/**
* Get configured table columns
*
* @return ?array<TableColumn>
*/
public function getTableColumns(): ?array
{
return $this->evaluate($this->tableColumns);
}
/**
* Determine if component should render as table
*/
public function isTable(): bool
{
return filled($this->getTableColumns());
}
/**
* @return array<Schema>
*/
public function getItems(): array
{
$containers = [];
foreach ($this->getState() ?? [] as $itemKey => $itemData) {
$container = $this
->getChildSchema()
->getClone()
->statePath($itemKey)
->inlineLabel(false);
if ($itemData instanceof Model) {
$container->record($itemData);
} elseif (is_array($itemData) || is_object($itemData)) {
$container->constantState($itemData);
}
$containers[$itemKey] = $container;
}
return $containers;
}
/**
* @return array<Schema>
*/
public function getDefaultChildSchemas(): array
{
return $this->getItems();
}
public function toEmbeddedHtml(): string
{
if ($this->isTable()) {
return $this->toEmbeddedTableHtml();
}
$items = $this->getItems();
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-repeatable',
'fi-contained' => $this->isContained(),
]);
if (empty($items)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$attributes = $attributes->grid($this->getGridColumns());
ob_start(); ?>
<ul <?= $attributes->toHtml() ?>>
<?php foreach ($items as $item) { ?>
<li class="fi-in-repeatable-item">
<?= $item->toHtml() ?>
</li>
<?php } ?>
</ul>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
protected function toEmbeddedTableHtml(): string
{
$items = $this->getItems();
$tableColumns = $this->getTableColumns();
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-table-repeatable',
]);
if (empty($items)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<table>
<thead>
<tr>
<?php foreach ($tableColumns as $column) { ?>
<th
class="<?= Arr::toCssClasses([
'fi-wrapped' => $column->canHeaderWrap(),
(($columnAlignment = $column->getAlignment()) instanceof Alignment) ? ('fi-align-' . $columnAlignment->value) : $columnAlignment,
]) ?>"
<?php if (filled($columnWidth = $column->getWidth())) { ?>
style="width: <?= $columnWidth ?>"
<?php } ?>
>
<?php if (! $column->isHeaderLabelHidden()) { ?>
<?= e($column->getLabel()) ?>
<?php } else { ?>
<span class="fi-sr-only">
<?= e($column->getLabel()) ?>
</span>
<?php } ?>
</th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item) { ?>
<tr>
<?php $counter = 0 ?>
<?php foreach ($item->getComponents(withHidden: true) as $component) { ?>
<?php throw_unless(
$component instanceof Component,
new Exception('Table repeatable entries must only contain schema components, but [' . $component::class . '] was used.'),
) ?>
<?php if (count($tableColumns) > $counter) { ?>
<?php $counter++ ?>
<?php if ($component->isVisible()) { ?>
<td>
<?= $component->toHtml() ?>
</td>
<?php } else { ?>
<td class="fi-hidden"></td>
<?php } ?>
<?php } ?>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
}
@@ -0,0 +1,49 @@
<?php
namespace Filament\Infolists\Components\RepeatableEntry;
use Closure;
use Filament\Support\Components\Component;
use Filament\Support\Concerns\CanWrapHeader;
use Filament\Support\Concerns\HasAlignment;
use Filament\Support\Concerns\HasWidth;
use Illuminate\Contracts\Support\Htmlable;
class TableColumn extends Component
{
use CanWrapHeader;
use HasAlignment;
use HasWidth;
protected string $evaluationIdentifier = 'column';
protected bool | Closure $isHeaderLabelHidden = false;
public function __construct(protected string | Htmlable | Closure $label) {}
public static function make(string | Htmlable | Closure $label): static
{
$static = app(static::class, ['label' => $label]);
$static->configure();
return $static;
}
public function hiddenHeaderLabel(bool | Closure $condition = true): static
{
$this->isHeaderLabelHidden = $condition;
return $this;
}
public function getLabel(): string | Htmlable
{
return $this->evaluate($this->label);
}
public function isHeaderLabelHidden(): bool
{
return (bool) $this->evaluate($this->isHeaderLabelHidden);
}
}
@@ -0,0 +1,646 @@
<?php
namespace Filament\Infolists\Components;
use Closure;
use Filament\Actions\Action;
use Filament\Infolists\View\Components\TextEntryComponent\ItemComponent;
use Filament\Infolists\View\Components\TextEntryComponent\ItemComponent\IconComponent;
use Filament\Schemas\Components\Contracts\HasAffixActions;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Concerns\CanBeCopied;
use Filament\Support\Concerns\CanWrap;
use Filament\Support\Concerns\HasFontFamily;
use Filament\Support\Concerns\HasLineClamp;
use Filament\Support\Concerns\HasWeight;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\FontFamily;
use Filament\Support\Enums\FontWeight;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\TextSize;
use Filament\Support\View\Components\BadgeComponent;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Js;
use Illuminate\View\ComponentAttributeBag;
use function Filament\Support\generate_href_html;
use function Filament\Support\generate_icon_html;
class TextEntry extends Entry implements HasAffixActions, HasEmbeddedView
{
use CanBeCopied;
use CanWrap;
use Concerns\CanFormatState;
use Concerns\HasAffixes;
use Concerns\HasColor;
use Concerns\HasIcon;
use Concerns\HasIconColor;
use HasFontFamily;
use HasLineClamp;
use HasWeight;
protected bool | Closure $isBadge = false;
protected bool | Closure $isBulleted = false;
protected bool | Closure $isProse = false;
protected bool | Closure $isListWithLineBreaks = false;
protected int | Closure | null $listLimit = null;
protected TextSize | string | Closure | null $size = null;
protected bool | Closure $isLimitedListExpandable = false;
public function badge(bool | Closure $condition = true): static
{
$this->isBadge = $condition;
return $this;
}
public function bulleted(bool | Closure $condition = true): static
{
$this->isBulleted = $condition;
return $this;
}
public function listWithLineBreaks(bool | Closure $condition = true): static
{
$this->isListWithLineBreaks = $condition;
return $this;
}
public function limitList(int | Closure | null $limit = 3): static
{
$this->listLimit = $limit;
return $this;
}
public function prose(bool | Closure $condition = true): static
{
$this->isProse = $condition;
return $this;
}
public function size(TextSize | string | Closure | null $size): static
{
$this->size = $size;
return $this;
}
public function getSize(mixed $state): TextSize | string
{
$size = $this->evaluate($this->size, [
'state' => $state,
]);
if (blank($size)) {
return TextSize::Small;
}
if (is_string($size)) {
$size = TextSize::tryFrom($size) ?? $size;
}
if ($size === 'base') {
return TextSize::Medium;
}
return $size;
}
public function isBadge(): bool
{
return (bool) $this->evaluate($this->isBadge);
}
public function isBulleted(): bool
{
return (bool) $this->evaluate($this->isBulleted);
}
public function isProse(): bool
{
return (bool) $this->evaluate($this->isProse);
}
public function isListWithLineBreaks(): bool
{
return $this->evaluate($this->isListWithLineBreaks) || $this->isBulleted();
}
public function getListLimit(): ?int
{
return $this->evaluate($this->listLimit);
}
public function expandableLimitedList(bool | Closure $condition = true): static
{
$this->isLimitedListExpandable = $condition;
return $this;
}
public function isLimitedListExpandable(): bool
{
return (bool) $this->evaluate($this->isLimitedListExpandable);
}
public function toEmbeddedHtml(): string
{
$isBadge = $this->isBadge();
$isListWithLineBreaks = $this->isListWithLineBreaks();
$isLimitedListExpandable = $this->isLimitedListExpandable();
$state = $this->getState();
if ($state instanceof Collection) {
$state = $state->all();
}
$attributes = $this->getExtraAttributeBag()
->class([
'fi-in-text',
]);
if (blank($state)) {
$attributes = $attributes
->merge([
'x-tooltip' => filled($tooltip = $this->getEmptyTooltip())
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false);
$placeholder = $this->getPlaceholder();
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if (filled($placeholder)) { ?>
<p class="fi-in-placeholder">
<?= e($placeholder) ?>
</p>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$shouldOpenUrlInNewTab = $this->shouldOpenUrlInNewTab();
$formatState = function (mixed $stateItem) use ($shouldOpenUrlInNewTab): string {
$url = $this->getUrl($stateItem);
$item = '';
if (filled($url)) {
$item .= '<a ' . generate_href_html($url, $shouldOpenUrlInNewTab)->toHtml() . '>';
}
$item .= e($this->formatState($stateItem));
if (filled($url)) {
$item .= '</a>';
}
return $item;
};
$state = Arr::wrap($state);
$stateCount = count($state);
$listLimit = $this->getListLimit() ?? $stateCount;
$stateOverListLimitCount = 0;
if ($listLimit && ($stateCount > $listLimit)) {
$stateOverListLimitCount = $stateCount - $listLimit;
if (
(! $isListWithLineBreaks) ||
(! $isLimitedListExpandable)
) {
$state = array_slice($state, 0, $listLimit);
}
}
if (($stateCount > 1) && (! $isListWithLineBreaks) && (! $isBadge)) {
$state = [
implode(
', ',
array_map(
fn (mixed $stateItem): string => $formatState($stateItem),
$state,
),
),
];
$stateCount = 1;
$formatState = fn (mixed $stateItem): string => $stateItem;
}
$alignment = $this->getAlignment();
$attributes = $attributes
->class([
'fi-in-text-has-badges' => $isBadge,
'fi-wrapped' => $this->canWrap(),
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
]);
$lineClamp = $this->getLineClamp();
$iconPosition = $this->getIconPosition();
$isBulleted = $this->isBulleted();
$isProse = $this->isProse();
$isMarkdown = $this->isMarkdown();
$getStateItem = function (mixed $stateItem) use ($iconPosition, $isBadge, $isMarkdown, $isProse, $lineClamp): array {
$color = $this->getColor($stateItem) ?? ($isBadge ? 'primary' : null);
$iconColor = $this->getIconColor($stateItem);
$size = $this->getSize($stateItem);
$iconHtml = generate_icon_html($this->getIcon($stateItem), attributes: (new ComponentAttributeBag)
->color(IconComponent::class, $iconColor), size: match ($size) {
TextSize::Medium => IconSize::Medium,
TextSize::Large => IconSize::Large,
default => IconSize::Small,
})?->toHtml();
$isCopyable = $this->isCopyable($stateItem);
if ($isCopyable) {
$copyableStateJs = Js::from($this->getCopyableState($stateItem) ?? $this->formatState($stateItem));
$copyMessageJs = Js::from($this->getCopyMessage($stateItem));
$copyMessageDurationJs = Js::from($this->getCopyMessageDuration($stateItem));
}
$tooltip = $this->getTooltip($stateItem);
return [
'attributes' => (new ComponentAttributeBag)
->class([
'fi-in-text-item',
'fi-prose' => $isProse || $isMarkdown,
(($fontFamily = $this->getFontFamily($stateItem)) instanceof FontFamily) ? "fi-font-{$fontFamily->value}" : (is_string($fontFamily) ? $fontFamily : ''),
])
->when(
! $isBadge,
fn (ComponentAttributeBag $attributes) => $attributes
->class([
($size instanceof TextSize) ? "fi-size-{$size->value}" : $size,
(($weight = $this->getWeight($stateItem)) instanceof FontWeight) ? "fi-font-{$weight->value}" : (is_string($weight) ? $weight : ''),
])
->when($lineClamp, fn (ComponentAttributeBag $attributes) => $attributes->style([
"--line-clamp: {$lineClamp}",
]))
->color(ItemComponent::class, $color)
),
'contentAttributes' => ($isBadge || $isCopyable || filled($tooltip))
? (new ComponentAttributeBag)
->merge([
'x-on:click' => $isCopyable
? <<<JS
window.navigator.clipboard.writeText({$copyableStateJs})
\$tooltip({$copyMessageJs}, {
theme: \$store.theme,
timeout: {$copyMessageDurationJs},
})
JS
: null,
'x-tooltip' => filled($tooltip)
? '{
content: ' . Js::from($tooltip) . ',
theme: $store.theme,
allowHTML: ' . Js::from($tooltip instanceof Htmlable) . ',
}'
: null,
], escape: false)
->class([
'fi-copyable' => $isCopyable,
])
->when(
$isBadge,
fn (ComponentAttributeBag $attributes) => $attributes
->class([
'fi-badge',
($size instanceof TextSize) ? "fi-size-{$size->value}" : $size,
])
->color(BadgeComponent::class, $color ?? 'primary')
)
: null,
'iconAfterHtml' => ($iconPosition === IconPosition::After) ? $iconHtml : '',
'iconBeforeHtml' => ($iconPosition === IconPosition::Before) ? $iconHtml : '',
];
};
$prefixActions = array_filter(
$this->getPrefixActions(),
fn (Action $prefixAction): bool => $prefixAction->isVisible(),
);
$suffixActions = array_filter(
$this->getSuffixActions(),
fn (Action $suffixAction): bool => $suffixAction->isVisible(),
);
if (
($stateCount === 1) &&
(! $isBulleted) &&
empty($prefixActions) &&
empty($suffixActions)
) {
$stateItem = Arr::first($state);
[
'attributes' => $stateItemAttributes,
'contentAttributes' => $stateItemContentAttributes,
'iconAfterHtml' => $stateItemIconAfterHtml,
'iconBeforeHtml' => $stateItemIconBeforeHtml,
] = $getStateItem($stateItem);
ob_start(); ?>
<div <?= $attributes
->merge($stateItemAttributes->getAttributes(), escape: false)
->toHtml() ?>>
<?php if ($stateItemContentAttributes) { ?>
<span <?= $stateItemContentAttributes->toHtml() ?>>
<?php } ?>
<?= $stateItemIconBeforeHtml ?>
<?= $formatState($stateItem) ?>
<?= $stateItemIconAfterHtml ?>
<?php if ($stateItemContentAttributes) { ?>
</span>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
$attributes = $attributes
->class([
'fi-bulleted' => $isBulleted,
'fi-in-text-has-line-breaks' => $isListWithLineBreaks,
]);
if ($stateOverListLimitCount || $prefixActions || $suffixActions) {
$attributes = $attributes
->merge([
'x-data' => $isLimitedListExpandable
? '{ isLimited: true }'
: null,
], escape: false)
->class([
'fi-in-text-affixed' => $prefixActions || $suffixActions,
'fi-in-text-list-limited' => $stateOverListLimitCount,
]);
ob_start(); ?>
<div <?= $attributes->toHtml() ?>>
<?php if ($prefixActions) { ?>
<div class="fi-in-text-affix">
<?php foreach ($prefixActions as $prefixAction) { ?>
<?= $prefixAction->toHtml() ?>
<?php } ?>
</div>
<?php } ?>
<?php if ($prefixActions || $suffixActions) { ?>
<div class="fi-in-text-affixed-content">
<?php } ?>
<ul>
<?php $stateIteration = 1; ?>
<?php foreach ($state as $stateItem) { ?>
<?php [
'attributes' => $stateItemAttributes,
'contentAttributes' => $stateItemContentAttributes,
'iconAfterHtml' => $stateItemIconAfterHtml,
'iconBeforeHtml' => $stateItemIconBeforeHtml,
] = $getStateItem($stateItem); ?>
<li
<?php if ($stateIteration > $listLimit) { ?>
x-show="! isLimited"
x-cloak
x-transition
<?php } ?>
<?= $stateItemAttributes->toHtml() ?>
>
<?php if ($stateItemContentAttributes) { ?>
<span <?= $stateItemContentAttributes->toHtml() ?>>
<?php } ?>
<?= $stateItemIconBeforeHtml ?>
<?= $formatState($stateItem) ?>
<?= $stateItemIconAfterHtml ?>
<?php if ($stateItemContentAttributes) { ?>
</span>
<?php } ?>
</li>
<?php $stateIteration++ ?>
<?php } ?>
</ul>
<?php if ($stateOverListLimitCount) { ?>
<div class="fi-in-text-list-limited-message">
<?php if ($isLimitedListExpandable) { ?>
<div
role="button"
x-on:click.prevent.stop="isLimited = false"
x-show="isLimited"
class="fi-link fi-size-xs"
>
<?= trans_choice('filament-infolists::components.entries.text.actions.expand_list', $stateOverListLimitCount) ?>
</div>
<div
role="button"
x-on:click.prevent.stop="isLimited = true"
x-cloak
x-show="! isLimited"
class="fi-link fi-size-xs"
>
<?= trans_choice('filament-infolists::components.entries.text.actions.collapse_list', $stateOverListLimitCount) ?>
</div>
<?php } else { ?>
<?= trans_choice('filament-infolists::components.entries.text.more_list_items', $stateOverListLimitCount) ?>
<?php } ?>
</div>
<?php } ?>
<?php if ($prefixActions || $suffixActions) { ?>
</div>
<?php } ?>
<?php if ($suffixActions) { ?>
<div class="fi-in-text-affix">
<?php foreach ($suffixActions as $suffixAction) { ?>
<?= $suffixAction->toHtml() ?>
<?php } ?>
</div>
<?php } ?>
</div>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
ob_start(); ?>
<ul <?= $attributes->toHtml() ?>>
<?php foreach ($state as $stateItem) { ?>
<?php [
'attributes' => $stateItemAttributes,
'contentAttributes' => $stateItemContentAttributes,
'iconAfterHtml' => $stateItemIconAfterHtml,
'iconBeforeHtml' => $stateItemIconBeforeHtml,
] = $getStateItem($stateItem); ?>
<li <?= $stateItemAttributes->toHtml() ?>>
<?php if ($stateItemContentAttributes) { ?>
<span <?= $stateItemContentAttributes->toHtml() ?>>
<?php } ?>
<?= $stateItemIconBeforeHtml ?>
<?= $formatState($stateItem) ?>
<?= $stateItemIconAfterHtml ?>
<?php if ($stateItemContentAttributes) { ?>
</span>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php return $this->wrapEmbeddedHtml(ob_get_clean());
}
public function canWrapByDefault(): bool
{
return true;
}
/**
* @param string | array<int | string, string | Closure> | Closure | null $relationship
*/
public function avg(string | array | Closure | null $relationship, string | Closure | null $column): static
{
$this->state(function (TextEntry $entry, ?Model $record) use ($relationship, $column): int | float | null {
if (blank($record)) {
return null;
}
$record->loadAvg(
$entry->evaluate($relationship),
$entry->evaluate($column),
);
return $record->getAttributeValue($entry->getName());
});
return $this;
}
/**
* @param string | array<int | string, string | Closure> | Closure | null $relationships
*/
public function counts(string | array | Closure | null $relationships): static
{
$this->state(function (TextEntry $entry, ?Model $record) use ($relationships): int | float | null {
if (blank($record)) {
return null;
}
$record->loadCount(
$entry->evaluate($relationships),
);
return $record->getAttributeValue($entry->getName());
});
return $this;
}
/**
* @param string | array<int | string, string | Closure> | Closure | null $relationship
*/
public function max(string | array | Closure | null $relationship, string | Closure | null $column): static
{
$this->state(function (TextEntry $entry, ?Model $record) use ($relationship, $column): int | float | null {
if (blank($record)) {
return null;
}
$record->loadMax(
$entry->evaluate($relationship),
$entry->evaluate($column),
);
return $record->getAttributeValue($entry->getName());
});
return $this;
}
/**
* @param string | array<int | string, string | Closure> | Closure | null $relationship
*/
public function min(string | array | Closure | null $relationship, string | Closure | null $column): static
{
$this->state(function (TextEntry $entry, ?Model $record) use ($relationship, $column): int | float | null {
if (blank($record)) {
return null;
}
$record->loadMin(
$entry->evaluate($relationship),
$entry->evaluate($column),
);
return $record->getAttributeValue($entry->getName());
});
return $this;
}
/**
* @param string | array<int | string, string | Closure> | Closure | null $relationship
*/
public function sum(string | array | Closure | null $relationship, string | Closure | null $column): static
{
$this->state(function (TextEntry $entry, ?Model $record) use ($relationship, $column): int | float | null {
if (blank($record)) {
return null;
}
$record->loadSum(
$entry->evaluate($relationship),
$entry->evaluate($column),
);
return $record->getAttributeValue($entry->getName());
});
return $this;
}
}
@@ -0,0 +1,5 @@
<?php
namespace Filament\Infolists\Components;
class ViewEntry extends Entry {}
@@ -0,0 +1,95 @@
<?php
namespace Filament\Infolists\Concerns;
use Filament\Actions\Action;
use Filament\Schemas\Schema;
trait InteractsWithInfolists /** @phpstan-ignore trait.unused */
{
/**
* @deprecated Use `getSchema()` instead.
*/
public function getInfolist(string $name): ?Schema
{
return $this->getSchema($name);
}
/**
* @deprecated Use `cacheSchema()` instead.
*/
protected function cacheInfolist(string $name, Schema $schema): ?Schema
{
return $this->cacheSchema($name, $schema);
}
/**
* @return array<string, Schema>
*
* @deprecated Use `getCachedSchemas()` instead.
*/
public function getCachedInfolists(): array
{
return $this->getCachedSchemas();
}
/**
* @deprecated Use `hasCachedSchema()` instead.
*/
protected function hasCachedInfolist(string $name): bool
{
return $this->hasCachedSchema($name);
}
/**
* @deprecated Use `callMountedAction()` instead.
*
* @param array<string, mixed> $arguments
*/
public function callMountedInfolistAction(array $arguments = []): mixed
{
return $this->callMountedAction($arguments);
}
/**
* @deprecated Use `mountAction()` instead.
*/
public function mountInfolistAction(string $name, ?string $component = null, ?string $infolist = null): mixed
{
return $this->mountAction($name, context: [
'schemaComponent' => "{$infolist}.{$component}",
]);
}
/**
* @deprecated Use `mountedActionShouldOpenModal()` instead.
*/
public function mountedInfolistActionShouldOpenModal(?Action $mountedAction = null): bool
{
return $this->mountedActionShouldOpenModal($mountedAction);
}
/**
* @deprecated Use `mountedActionHasForm()` instead.
*/
public function mountedInfolistActionHasForm(?Action $mountedAction = null): bool
{
return $this->mountedActionHasSchema($mountedAction);
}
/**
* @deprecated Use `getMountedAction()` instead.
*/
public function getMountedInfolistAction(): ?Action
{
return $this->getMountedAction();
}
/**
* @deprecated Use `getMountedActionComponent()` instead.
*/
public function unmountInfolistAction(bool $shouldCancelParentActions = true): void
{
$this->unmountAction($shouldCancelParentActions);
}
}
@@ -0,0 +1,5 @@
<?php
namespace Filament\Infolists\Contracts;
interface HasInfolists {}
@@ -0,0 +1,36 @@
<?php
namespace Filament\Infolists;
use Filament\Infolists\Testing\TestsInfolistActions;
use Illuminate\Filesystem\Filesystem;
use Livewire\Features\SupportTesting\Testable;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class InfolistsServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('filament-infolists')
->hasCommands([
Commands\MakeEntryCommand::class,
])
->hasTranslations()
->hasViews();
}
public function packageBooted(): void
{
if ($this->app->runningInConsole()) {
foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) {
$this->publishes([
$file->getRealPath() => base_path("stubs/filament/{$file->getFilename()}"),
], 'filament-stubs');
}
}
Testable::mixin(new TestsInfolistActions);
}
}
@@ -0,0 +1,348 @@
<?php
namespace Filament\Infolists\Testing;
use BackedEnum;
use Closure;
use Filament\Schemas\Contracts\HasSchemas;
use Livewire\Component;
use Livewire\Features\SupportTesting\Testable;
/**
* @method Component&HasSchemas instance()
*
* @mixin Testable
*/
class TestsInfolistActions
{
public function mountInfolistAction(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->mountAction($actions);
return $this;
};
}
public function unmountInfolistAction(): Closure
{
return function (): static {
$this->unmountAction();
return $this;
};
}
public function setInfolistActionData(): Closure
{
return function (array $data): static {
$this->fillForm($data);
return $this;
};
}
public function assertInfolistActionDataSet(): Closure
{
return function (array | Closure $data): static {
$this->assertSchemaStateSet($data);
return $this;
};
}
public function callInfolistAction(): Closure
{
return function (string $component, string | array $actions, array $data = [], array $arguments = [], string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema, $arguments);
$this->callAction($actions, $data);
return $this;
};
}
public function callMountedInfolistAction(): Closure
{
return function (array $arguments = []): static {
$this->callMountedAction($arguments);
return $this;
};
}
public function assertInfolistActionExists(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionExists($actions);
return $this;
};
}
public function assertInfolistActionDoesNotExist(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDoesNotExist($actions);
return $this;
};
}
public function assertInfolistActionVisible(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionVisible($actions);
return $this;
};
}
public function assertInfolistActionHidden(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionHidden($actions);
return $this;
};
}
public function assertInfolistActionEnabled(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionEnabled($actions);
return $this;
};
}
public function assertInfolistActionDisabled(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDisabled($actions);
return $this;
};
}
public function assertInfolistActionHasIcon(): Closure
{
return function (string $component, string | array $actions, string | BackedEnum $icon, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionHasIcon($actions, $icon);
return $this;
};
}
public function assertInfolistActionDoesNotHaveIcon(): Closure
{
return function (string $component, string | array $actions, string | BackedEnum $icon, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDoesNotHaveIcon($actions, $icon);
return $this;
};
}
public function assertInfolistActionHasLabel(): Closure
{
return function (string $component, string | array $actions, string $label, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionHasLabel($actions, $label);
return $this;
};
}
public function assertInfolistActionDoesNotHaveLabel(): Closure
{
return function (string $component, string | array $actions, string $label, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDoesNotHaveLabel($actions, $label);
return $this;
};
}
public function assertInfolistActionHasColor(): Closure
{
return function (string $component, string | array $actions, string | array $color, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionHasColor($actions, $color);
return $this;
};
}
public function assertInfolistActionDoesNotHaveColor(): Closure
{
return function (string $component, string | array $actions, string | array $color, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDoesNotHaveColor($actions, $color);
return $this;
};
}
public function assertInfolistActionHasUrl(): Closure
{
return function (string $component, string | array $actions, string $url, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionHasUrl($actions, $url);
return $this;
};
}
public function assertInfolistActionDoesNotHaveUrl(): Closure
{
return function (string $component, string | array $actions, string $url, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionDoesNotHaveUrl($actions, $url);
return $this;
};
}
public function assertInfolistActionShouldOpenUrlInNewTab(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionShouldOpenUrlInNewTab($actions);
return $this;
};
}
public function assertInfolistActionShouldNotOpenUrlInNewTab(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionShouldNotOpenUrlInNewTab($actions);
return $this;
};
}
public function assertInfolistActionMounted(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionMounted($actions);
return $this;
};
}
public function assertInfolistActionNotMounted(): Closure
{
return function (string $component, string | array $actions, string $schema = 'infolist'): static {
/** @var array<array<string, mixed>> $actions */
/** @phpstan-ignore-next-line */
$actions = $this->parseNestedInfolistActions($component, $actions, $schema);
$this->assertActionNotMounted($actions);
return $this;
};
}
public function assertInfolistActionHalted(): Closure
{
return $this->assertInfolistActionMounted();
}
public function assertHasInfolistActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasFormErrors($keys);
return $this;
};
}
public function assertHasNoInfolistActionErrors(): Closure
{
return function (array $keys = []): static {
$this->assertHasNoFormErrors($keys);
return $this;
};
}
public function parseNestedInfolistActions(): Closure
{
return function (string $component, string | array $actions, string $infolist, array $arguments = []): array {
/** @var array<array<string, mixed>> $actions */
$actions = $this->parseNestedActions($actions, $arguments);
$actions[array_key_first($actions)]['context']['schemaComponent'] = "{$infolist}.{$component}";
return $actions;
};
}
}
@@ -0,0 +1,61 @@
<?php
namespace Filament\Infolists\View\Components\IconEntryComponent;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class IconComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
$gray = FilamentColor::getColor('gray');
/**
* Since the icons in the entry are the only content, they should have a color that contrasts
* at least 3:1 with the background to remain compliant with WCAG AA standards.
*
* @ref https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html
*/
ksort($color);
foreach (array_keys($color) as $shade) {
if (Color::isNonTextContrastRatioAccessible('oklch(1 0 0)', $color[$shade])) {
$text = $shade;
break;
}
}
$text ??= 900;
krsort($color);
$lightestDarkGrayBg = $gray[800];
foreach (array_keys($color) as $shade) {
if ($shade > 600) {
continue;
}
if (Color::isNonTextContrastRatioAccessible($lightestDarkGrayBg, $color[$shade])) {
$darkText = $shade;
break;
}
}
$darkText ??= 200;
return [
'text' => $text,
'dark:text' => $darkText,
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace Filament\Infolists\View\Components\TextEntryComponent;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class ItemComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
$gray = FilamentColor::getColor('gray');
ksort($color);
foreach (array_keys($color) as $shade) {
if (Color::isTextContrastRatioAccessible('oklch(1 0 0)', $color[$shade])) {
$text = $shade;
break;
}
}
$text ??= 900;
krsort($color);
$lightestDarkGrayBg = $gray[800];
foreach (array_keys($color) as $shade) {
if ($shade > 600) {
continue;
}
if (Color::isTextContrastRatioAccessible($lightestDarkGrayBg, $color[$shade])) {
$darkText = $shade;
break;
}
}
$darkText ??= 200;
return [
'text' => $text,
'dark:text' => $darkText,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Infolists\View\Components\TextEntryComponent\ItemComponent;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class IconComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}

Some files were not shown because too many files have changed in this diff Show More