Handles dynamic forms builder schemas, layout components (grid/section/fieldset/tabs/split/placeholder), field types (TextInput, Select, Toggle), validation, and lifecycle hooks in Kinetix. Activates when creating, modifying, or rendering form views.
php artisan boost:add-skill happones/kinetix
Save this content to: .claude/skills/kinetix-forms/SKILL.md
# Kinetix Forms Development
## When to Apply
Activate this skill when:
- Building input forms for resource pages or action modals.
- Adding fields like `TextInput`, `NumberField` (steppers + decimal/percent/currency), `Slider`, `Rating` (stars + half), `PinInput` (OTP), `SlugInput` (auto from a source field), `SignaturePad` (canvas → PNG data URL), `PhoneInput` (international, country selector + E.164), `Select`, `Checkbox`, `Toggle`, `DateTimePicker`, `DateRangePicker`, `AddressPicker`, `RichEditor` (WYSIWYG: basic/tiptap/markdown), `Textarea`, `Hidden`, `Radio`, `CheckboxList`, `ColorPicker`, `TagsInput`, `KeyValue`, `Repeater`, or `FileUpload`. Date/Month/Week/Range pickers and `NumberField` default their calendar/number locale to the **application locale** (BCP-47); `->locale('fr')` overrides per field. Date/DateTime/Time pickers: values are naive wall-time strings whose implicit timezone is **`app.timezone`** — the Today/Now presets read the clock there (never the browser's), overridable per field with `->timezone('America/Mexico_City')`. DateTime/Time popovers carry a Now + Done footer; `->confirm()` switches to an explicit **Apply** commit (draft until then); DatePicker adds `->todayButton()` and `->closeOnSelect(false)`. Date/DateTime/Time/Month pickers take `->typed('dd/mm/yyyy')` (or `->typed()` for the locale order): a masked keyboard input with the picker on a button to its right; tokens `dd` `mm` `yyyy`/`yy` `HH`/`hh` `mm` `a` (`mm` = minutes after the first space/hour token); the stored value stays ISO, an impossible/out-of-range/half-typed text holds `null` and shows a message under the field; a format missing a part throws. `CheckboxList::make()->bulkToggleable()` adds a tri-state **Select all** (with a `2/5` count) over the options in view — with `->searchable()`, only the ones the search left. `MultiSelectFilter::make()->bulkToggleable()` gives table filters the same.
- Structuring layouts: `Grid::make(n)`, `Section::make()` (card), `Fieldset::make()` (bordered legend), `Tabs::make()->tabs([Tab::make()->icon()->schema()])`, `Split::make([...])` (responsive flex row), `Placeholder::make('Label')->content()` or `Placeholder::make('name')->label(...)->content()` (read-only, not a field), `Wizard::make()->steps([Step::make()])` (multi-step — see the `kinetix-wizard` skill). All nest and share `columnSpan()`/`columnSpanFull()` (shorthand for `columnSpan('full')`)/`visible()`/`hidden()`.
- Addresses: `AddressPicker::make('address')` is free text (inputs + ISO country select); `->locations()` (or `->locations('countries')`, a provider name / LocationProvider class) turns state and city into chained selects (country → state → city; changing a parent clears its children; a level with nothing in the catalog falls back to typing). Providers live in `kinetix.locations.providers`: `world` (nnjeim/world, default) and `countries` (altwaireb/laravel-countries) via the `database` driver (table/column mapping, `active`, `soft_deletes` — point it at your own tables too), or `driver => YourProvider::class` implementing `Happones\Kinetix\Locations\LocationProvider` (countries/states/cities/stateLabel/cityLabel) for an API or any source. Value adds `stateId`/`cityId`; names are rewritten from the provider on save. Self-validating (`AddressRule`: parents/children belong together, `->requiredFields([...])`, country in list). Lists/labels cached (`cache_ttl`), cities paged + server-searched past `cities_limit`; index `states.country_id` / `cities.state_id` on nnjeim/world. Postal code first: `->locations('sepomex')->postalCodeFirst()` (provider implements `Locations\PostalCodeProvider`: postalCode/neighborhoods/neighborhood — the database driver does when its entry maps a `postal_codes` table with code/name/state/city): typing the code fills state + city and narrows the `neighborhood` level (opt-in part; value adds `neighborhood`/`neighborhoodId`), picking a neighborhood fills the code; name + code rewritten from the provider on save; `postalCodeFirst()` on a provider without postal codes throws LogicException.
- Weekly business hours: `BusinessHours::make('hours')` — per-day switch + HH:MM ranges (Kinetix time pickers; `->twentyFourHour()`, `->minuteStep(15)`) + apply-to-all editor (Intl day names); defaults M–F 09:00–17:00; auto-validated (`kinetix_weekly_schedule` rule, also usable standalone). Pair with `AsWeeklySchedule` cast → `$model->hours?->isOpenAt(now($tz))` / `isOpenNow($tz)` / `effectiveSchedule()` / `rangesFor('monday')`; end ≤ start wraps overnight (fri 22:00–02:00 covers sat 01:00). Shape: `{monday: {enabled, ranges: [{start, end}]}, …}`, all 7 days always present.
- Adding Laravel validation rules dynamically to inputs (`required()`, `maxLength()`, `rules()`), custom messages (`validationMessages()`), or attribute names (`validationAttribute()`).
- Validating either **fluently** (`$form->validate()`) or via a **FormRequest** (`KinetixFormRequest` / the `ResolvesKinetixForm` trait) — rules live in the form, never duplicated.
- Enabling **live validation** with Laravel Precognition (`->precognitive()` / `->validationUrl()`).
- Surfacing validation errors inside **Tabs/Wizards** (auto-switch to the offending tab/step, focus the first errored field).
- Deep-linking a tab: the open tab is in the URL by default (`?tab=security`, the label's slug; nested groups share the param). `Tab::make(__('Security'))->key('security')` for translated labels, `->persistInQueryString('section'|false)` per group, config `kinetix.forms.query_string.tabs` (also covers infolist tabs). Never inside a modal.
- Using lifecycle hooks (`afterStateHydrated()`, `dehydrateStateUsing()`).
## UI reuse (DRY — REQUIRED)
Never re-write a component's classes to imitate an existing Kinetix component:
- **Buttons**: use `<KinetixButton variant="…" size="…" :loading :disabled>` —
it owns the shadcn recipe plus the pending contract (loading → disabled +
spinner + aria-busy). Map an action's status color with
`actionButtonVariant(color)`. Only when a component genuinely can't be used
(a `<DropdownMenuTrigger>`, a link) compose classes with
`buttonVariants({ variant, size })` from `useKinetixShadcnVariants` — never
a hand-copied class string.
- **Modals**: build on `primitives/KinetixModal.vue` (the shadcn new-york-v4
dialog shell: overlay/panel animation, header/footer stacks, close button,
focus trap, Kinetix z-scale) — never a hand-rolled Teleport + overlay div.
- **Checkboxes/selects/inputs**: `<KinetixCheckbox>`, `<KinetixSelect>`, the
form field components.
A duplicated class string is a bug: when the base component evolves, the copy
silently drifts.
## Documentation
For full details, reference the [Kinetix Forms Documentation](https://happones.github.io/kinetix/forms).
## Localizing labels
Any display string you set is **your app's copy** — wrap it in Laravel's `__()` so
it's translatable: `->label()`, `->placeholder()`, `->helperText()`, section/tab
headings, and `Select`/radio **option labels**
(`TextInput::make('email')->label(__('posts.fields.email'))`). Fields with no
explicit `->label()` are auto-humanized. See the **kinetix-locale** skill.
## Usage Guide
### 1. Form Schema Definition
```php
use Happones\Kinetix\Forms\Form;
use Happones\Kinetix\Forms\Components\Grid;
use Happones\Kinetix\Forms\Components\Section;
use Happones\Kinetix\Forms\Components\TextInput;
use Happones\Kinetix\Forms\Components\Select;
use App\Models\Post;
$form = Form::make(Post::make())
->schema([
Section::make('Post Metadata')
->description('Specify basic post descriptors.')
->schema([
Grid::make(12)->schema([
TextInput::make('title')
->required()
->maxLength(255)
->columnSpan(8),
Select::make('category_id')
->options(Category::pluck('name', 'id')->all())
->columnSpan(4),
]),
]),
]);
```
### 2. Validation & Submission
Two supported paths — pick per endpoint; **rules always live in the form**, never duplicated.
An optional field (no `required()`) that carries rules is validated as `nullable` automatically, so clearing an optional `email()`/`url()`/`numeric()` field passes — don't add `->rules(['nullable'])` by hand. Rule objects (`Password::min(8)`, `Rule::unique()`) can go straight into `->rules([...])`.
**Fluent** (inline in the controller):
```php
public function store(Request $request)
{
$form = $this->buildForm();
$form->validate($request->all()); // rules + messages + attributes
Post::create($form->getState($request->all())); // dehydrated state
return redirect()->route('posts.index');
}
```
**FormRequest bridge** (for `authorize()`, `prepareForValidation()`, Precognition):
```php
use Happones\Kinetix\Forms\Http\KinetixFormRequest;
class StorePostRequest extends KinetixFormRequest
{
protected function form(): Form { return PostForm::make()->model(Post::class); }
}
// Controller — dehydratedState() = validated + dehydrated (drops saved(false) fields):
public function store(StorePostRequest $request)
{
Post::create($request->dehydratedState());
}
```
Already extending another base request? `use ResolvesKinetixForm;` + implement `form()` — same API.
### 3. Live Validation (Precognition)
Reuse the FormRequest rules to validate fields as the user types — **no extra npm dependency** (built-in client):
```php
// Route: add the middleware so Precognition has rules to run.
Route::post('/posts', [PostController::class, 'store'])
->middleware(\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class);
// Form: opt in.
PostForm::make()->precognitive(); // validate against the submit URL
PostForm::make()->validationUrl(route('posts.store'), 'post'); // or an explicit endpoint
```
```vue
<KinetixForm :form="postForm" validation-url="/posts" @submit="submit" />
```
### Submitted from outside: `id` + `hide-submit`
A page footer or header can own Save: `<KinetixForm id="adjust-stock" hide-submit @submit="...">`
and `Action::make('save')->submits('adjust-stock')` in the bar.
### 4. Forms inside modals: pass `flat`
When a `KinetixForm` renders inside a `KinetixModal`/`KinetixSheet`, ALWAYS
pass the `flat` prop: the panel is already the surface, so Sections render as
divided groups (heading + description intact) instead of nesting a card
inside the modal. Kinetix's own modal hosts (record modals, attach forms,
view-modal infolists via `KinetixInfolist`'s own `flat`) do this
automatically; host pages composing their own modal must do it themselves.
```vue
<KinetixModal :open="open" title="New post" scroll-body>
<KinetixForm :form="postForm" flat @submit="submit" />
</KinetixModal>
```
`scroll-body` is a LAYOUT choice, not a rescue: the shell bounds itself, so a
panel taller than the viewport scrolls inside the dialog wrapper either way.
Pass it when the header and footer should stay **pinned** while only the body
scrolls (shadcn `ScrollArea`) — the usual choice for a long form, so Save stays
visible. Skip it when the body hosts an in-flow dropdown that must escape the
panel. `KinetixSheet` always pins.
### 5. Error Focus in Tabs & Wizards
`KinetixForm` reads Inertia's `errors` automatically (a controller `ValidationException` now renders with no wiring). Errored **tabs/wizard steps** are marked, the form **switches/jumps to the first offending one**, and the **first errored field is focused + scrolled into view** — fully recursive (wizard-in-tab-in-section resolves correctly). Live per-field validation never steals focus from the field being edited.
---
### 6. Conditional fields and reactivity
- `visibleWhen/hiddenWhen/requiredWhen/disabledWhen($field, $value, $operator)`
ship as data: the browser and the server evaluate them with the SAME rules
(against a boolean → boolean, an untouched toggle is false; else text, `1` =
`'1'`, `null` = `''`; a list value matches when it contains the value). An
unknown operator throws; `in`/`notIn` need a list. A conditionally hidden
field is never validated nor persisted, on every path (FormRequest too).
- `->live()` + `afterStateUpdated(fn (Set $set) => …)` + `options(fn (Get $get)
=> …)` need a reconstructible form (a `Form` subclass or a resource form).
Only the changed live field's hook runs per recompute, so cascades work;
`Get` reads the filled values on the first render as well.
## Best Practices
- **Constructor Property Injection**: Ensure all fields use promotion parameter reflection.
- **Dynamic Closures**: Leverage closures on field configurations (e.g. `disabled(fn() => !auth()->user()->isAdmin())`) to make forms dynamic to active contexts.
- **Option order is kept**: `options(State::orderBy('name')->pluck('name', 'id'))` renders in that order — when a JS object would re-sort the integer-like keys, Select/Radio/CheckboxList/filters/remote search send a list of `{value, label}` instead of the map (`OptionList::forWire()`); published components read both (`kinetixOptionList()`). Iterate that list in custom components, never the object.
- **Select Option Mapping**: Use native Enum reflection mapping directly (e.g. `options(PostStatus::class)` + `->default(PostStatus::Draft->value)`) to easily bind Enums cast on models. Labels resolve from `getLabel()` when the enum implements `Happones\Kinetix\Support\Contracts\HasLabel` — or simply defines a public `getLabel(): ?string` method (duck-typed, so enums written against equivalent third-party label contracts work unchanged); enums without one fall back to the backing `value`/case `name`. Add the `Happones\Kinetix\Support\Concerns\HasLabelOptions` concern for a static `PostStatus::options()` array helper. Define the enum once: the same class drives `Select`/`Radio`, `TextColumn->badge()` (with `HasColor`/`HasIcon`), `SelectColumn`, `SelectFilter`, Infolist entries, and export columns.
- **Eager Refactoring**: Avoid complex inline HTML attributes, use `extraInputAttributes()` to inject Tailwind classes or custom settings cleanly.
- **Role/permission-gated fields**: use `->authorize(string $ability, mixed $subject = null)` (Gate-based, same shorthand as `Action::authorize()`) alongside `->visible()`/`->hidden()`. Without an explicit `$subject`, a record-dependent ability defers to visible on `create` (no record yet) and is checked normally on `edit`. Unauthorized fields are dropped from validation, hydration, and the serialized payload — never rely on hiding a field client-side only.
## Responsive Layout
Grids are responsive by default: `Grid::make(2)` /
`->columns(2)` means 2 columns from `lg` up and ONE below — never assume a
fixed column count on mobile. `columns()` and `columnSpan()` accept breakpoint
maps (`['default' => 1, 'sm' => 2, 'xl' => 3]`, keys default/sm/md/lg/xl/2xl,
values carry forward) on Grid/Section/Fieldset/Tab/Step. Breakpoints measure
the FORM's own width (CSS container queries), so forms in modals collapse
correctly; spans clamp to the available columns per breakpoint and can never
overflow. `columnSpan('full')` spans the row at every size.
Activate this skill when:
TextInput, NumberField (steppers + decimal/percent/currency), Slider, Rating (stars + half), PinInput (OTP), SlugInput (auto from a source field), SignaturePad (canvas → PNG data URL), PhoneInput (international, country selector + E.164), Select, Checkbox, Toggle, DateTimePicker, DateRangePicker, AddressPicker, RichEditor (WYSIWYG: basic/tiptap/markdown), Textarea, Hidden, Radio, CheckboxList, ColorPicker, TagsInput, KeyValue, Repeater, or FileUpload. Date/Month/Week/Range pickers and NumberField default their calendar/number locale to the application locale (BCP-47); ->locale('fr') overrides per field. Date/DateTime/Time pickers: values are naive wall-time strings whose implicit timezone is app.timezone — the Today/Now presets read the clock there (never the browser's), overridable per field with ->timezone('America/Mexico_City'). DateTime/Time popovers carry a Now + Done footer; ->confirm() switches to an explicit Apply commit (draft until then); DatePicker adds ->todayButton() and ->closeOnSelect(false). Date/DateTime/Time/Month pickers take ->typed('dd/mm/yyyy') (or ->typed() for the locale order): a masked keyboard input with the picker on a button to its right; tokens dd mm yyyy/yy HH/hh mm a (mm = minutes after the first space/hour token); the stored value stays ISO, an impossible/out-of-range/half-typed text holds null and shows a message under the field; a format missing a part throws. CheckboxList::make()->bulkToggleable() adds a tri-state Select all (with a 2/5 count) over the options in view — with ->searchable(), only the ones the search left. MultiSelectFilter::make()->bulkToggleable() gives table filters the same.Grid::make(n), Section::make() (card), Fieldset::make() (bordered legend), Tabs::make()->tabs([Tab::make()->icon()->schema()]), Split::make([...]) (responsive flex row), Placeholder::make('Label')->content() or Placeholder::make('name')->label(...)->content() (read-only, not a field), Wizard::make()->steps([Step::make()]) (multi-step — see the kinetix-wizard skill). All nest and share columnSpan()/columnSpanFull() (shorthand for columnSpan('full'))/visible()/hidden().AddressPicker::make('address') is free text (inputs + ISO country select); ->locations() (or ->locations('countries'), a provider name / LocationProvider class) turns state and city into chained selects (country → state → city; changing a parent clears its children; a level with nothing in the catalog falls back to typing). Providers live in kinetix.locations.providers: world (nnjeim/world, default) and countries (altwaireb/laravel-countries) via the database driver (table/column mapping, active, soft_deletes — point it at your own tables too), or driver => YourProvider::class implementing Happones\Kinetix\Locations\LocationProvider (countries/states/cities/stateLabel/cityLabel) for an API or any source. Value adds stateId/cityId; names are rewritten from the provider on save. Self-validating (AddressRule: parents/children belong together, ->requiredFields([...]), country in list). Lists/labels cached (cache_ttl), cities paged + server-searched past cities_limit; index states.country_id / cities.state_id on nnjeim/world. Postal code first: ->locations('sepomex')->postalCodeFirst() (provider implements Locations\PostalCodeProvider: postalCode/neighborhoods/neighborhood — the database driver does when its entry maps a postal_codes table with code/name/state/city): typing the code fills state + city and narrows the neighborhood level (opt-in part; value adds neighborhood/neighborhoodId), picking a neighborhood fills the code; name + code rewritten from the provider on save; postalCodeFirst() on a provider without postal codes throws LogicException.BusinessHours::make('hours') — per-day switch + HH:MM ranges (Kinetix time pickers; ->twentyFourHour(), ->minuteStep(15)) + apply-to-all editor (Intl day names); defaults M–F 09:00–17:00; auto-validated (kinetix_weekly_schedule rule, also usable standalone). Pair with AsWeeklySchedule cast → $model->hours?->isOpenAt(now($tz)) / isOpenNow($tz) / effectiveSchedule() / rangesFor('monday'); end ≤ start wraps overnight (fri 22:00–02:00 covers sat 01:00). Shape: {monday: {enabled, ranges: [{start, end}]}, …}, all 7 days always present.required(), maxLength(), rules()), custom messages (validationMessages()), or attribute names (validationAttribute()).$form->validate()) or via a FormRequest (KinetixFormRequest / the ResolvesKinetixForm trait) — rules live in the form, never duplicated.->precognitive() / ->validationUrl()).?tab=security, the label's slug; nested groups share the param). Tab::make(__('Security'))->key('security') for translated labels, ->persistInQueryString('section'|false) per group, config kinetix.forms.query_string.tabs (also covers infolist tabs). Never inside a modal.afterStateHydrated(), dehydrateStateUsing()).Never re-write a component's classes to imitate an existing Kinetix component:
<KinetixButton variant="…" size="…" :loading :disabled> —
it owns the shadcn recipe plus the pending contract (loading → disabled +
spinner + aria-busy). Map an action's status color with
actionButtonVariant(color). Only when a component genuinely can't be used
(a <DropdownMenuTrigger>, a link) compose classes with
buttonVariants({ variant, size }) from useKinetixShadcnVariants — never
a hand-copied class string.primitives/KinetixModal.vue (the shadcn new-york-v4
dialog shell: overlay/panel animation, header/footer stacks, close button,
focus trap, Kinetix z-scale) — never a hand-rolled Teleport + overlay div.<KinetixCheckbox>, <KinetixSelect>, the
form field components.A duplicated class string is a bug: when the base component evolves, the copy silently drifts.
For full details, reference the Kinetix Forms Documentation.
Any display string you set is your app's copy — wrap it in Laravel's __() so
it's translatable: ->label(), ->placeholder(), ->helperText(), section/tab
headings, and Select/radio option labels
(TextInput::make('email')->label(__('posts.fields.email'))). Fields with no
explicit ->label() are auto-humanized. See the kinetix-locale skill.
use Happones\Kinetix\Forms\Form;
use Happones\Kinetix\Forms\Components\Grid;
use Happones\Kinetix\Forms\Components\Section;
use Happones\Kinetix\Forms\Components\TextInput;
use Happones\Kinetix\Forms\Components\Select;
use App\Models\Post;
$form = Form::make(Post::make())
->schema([
Section::make('Post Metadata')
->description('Specify basic post descriptors.')
->schema([
Grid::make(12)->schema([
TextInput::make('title')
->required()
->maxLength(255)
->columnSpan(8),
Select::make('category_id')
->options(Category::pluck('name', 'id')->all())
->columnSpan(4),
]),
]),
]);
Two supported paths — pick per endpoint; rules always live in the form, never duplicated.
An optional field (no required()) that carries rules is validated as nullable automatically, so clearing an optional email()/url()/numeric() field passes — don't add ->rules(['nullable']) by hand. Rule objects (Password::min(8), Rule::unique()) can go straight into ->rules([...]).
Fluent (inline in the controller):
public function store(Request $request)
{
$form = $this->buildForm();
$form->validate($request->all()); // rules + messages + attributes
Post::create($form->getState($request->all())); // dehydrated state
return redirect()->route('posts.index');
}
FormRequest bridge (for authorize(), prepareForValidation(), Precognition):
use Happones\Kinetix\Forms\Http\KinetixFormRequest;
class StorePostRequest extends KinetixFormRequest
{
protected function form(): Form { return PostForm::make()->model(Post::class); }
}
// Controller — dehydratedState() = validated + dehydrated (drops saved(false) fields):
public function store(StorePostRequest $request)
{
Post::create($request->dehydratedState());
}
Already extending another base request? use ResolvesKinetixForm; + implement form() — same API.
Reuse the FormRequest rules to validate fields as the user types — no extra npm dependency (built-in client):
// Route: add the middleware so Precognition has rules to run.
Route::post('/posts', [PostController::class, 'store'])
->middleware(\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class);
// Form: opt in.
PostForm::make()->precognitive(); // validate against the submit URL
PostForm::make()->validationUrl(route('posts.store'), 'post'); // or an explicit endpoint
<KinetixForm :form="postForm" validation-url="/posts" @submit="submit" />
id + hide-submitA page footer or header can own Save: <KinetixForm id="adjust-stock" hide-submit @submit="...">
and Action::make('save')->submits('adjust-stock') in the bar.
flatWhen a KinetixForm renders inside a KinetixModal/KinetixSheet, ALWAYS
pass the flat prop: the panel is already the surface, so Sections render as
divided groups (heading + description intact) instead of nesting a card
inside the modal. Kinetix's own modal hosts (record modals, attach forms,
view-modal infolists via KinetixInfolist's own flat) do this
automatically; host pages composing their own modal must do it themselves.
<KinetixModal :open="open" title="New post" scroll-body>
<KinetixForm :form="postForm" flat @submit="submit" />
</KinetixModal>
scroll-body is a LAYOUT choice, not a rescue: the shell bounds itself, so a
panel taller than the viewport scrolls inside the dialog wrapper either way.
Pass it when the header and footer should stay pinned while only the body
scrolls (shadcn ScrollArea) — the usual choice for a long form, so Save stays
visible. Skip it when the body hosts an in-flow dropdown that must escape the
panel. KinetixSheet always pins.
KinetixForm reads Inertia's errors automatically (a controller ValidationException now renders with no wiring). Errored tabs/wizard steps are marked, the form switches/jumps to the first offending one, and the first errored field is focused + scrolled into view — fully recursive (wizard-in-tab-in-section resolves correctly). Live per-field validation never steals focus from the field being edited.
visibleWhen/hiddenWhen/requiredWhen/disabledWhen($field, $value, $operator)
ship as data: the browser and the server evaluate them with the SAME rules
(against a boolean → boolean, an untouched toggle is false; else text, 1 =
'1', null = ''; a list value matches when it contains the value). An
unknown operator throws; in/notIn need a list. A conditionally hidden
field is never validated nor persisted, on every path (FormRequest too).->live() + afterStateUpdated(fn (Set $set) => …) + options(fn (Get $get) => …) need a reconstructible form (a Form subclass or a resource form).
Only the changed live field's hook runs per recompute, so cascades work;
Get reads the filled values on the first render as well.disabled(fn() => !auth()->user()->isAdmin())) to make forms dynamic to active contexts.options(State::orderBy('name')->pluck('name', 'id')) renders in that order — when a JS object would re-sort the integer-like keys, Select/Radio/CheckboxList/filters/remote search send a list of {value, label} instead of the map (OptionList::forWire()); published components read both (kinetixOptionList()). Iterate that list in custom components, never the object.options(PostStatus::class) + ->default(PostStatus::Draft->value)) to easily bind Enums cast on models. Labels resolve from getLabel() when the enum implements Happones\Kinetix\Support\Contracts\HasLabel — or simply defines a public getLabel(): ?string method (duck-typed, so enums written against equivalent third-party label contracts work unchanged); enums without one fall back to the backing value/case name. Add the Happones\Kinetix\Support\Concerns\HasLabelOptions concern for a static PostStatus::options() array helper. Define the enum once: the same class drives Select/Radio, TextColumn->badge() (with HasColor/HasIcon), SelectColumn, SelectFilter, Infolist entries, and export columns.extraInputAttributes() to inject Tailwind classes or custom settings cleanly.->authorize(string $ability, mixed $subject = null) (Gate-based, same shorthand as Action::authorize()) alongside ->visible()/->hidden(). Without an explicit $subject, a record-dependent ability defers to visible on create (no record yet) and is checked normally on edit. Unauthorized fields are dropped from validation, hydration, and the serialized payload — never rely on hiding a field client-side only.Grids are responsive by default: Grid::make(2) /
->columns(2) means 2 columns from lg up and ONE below — never assume a
fixed column count on mobile. columns() and columnSpan() accept breakpoint
maps (['default' => 1, 'sm' => 2, 'xl' => 3], keys default/sm/md/lg/xl/2xl,
values carry forward) on Grid/Section/Fieldset/Tab/Step. Breakpoints measure
the FORM's own width (CSS container queries), so forms in modals collapse
correctly; spans clamp to the available columns per breakpoint and can never
overflow. columnSpan('full') spans the row at every size.
How can I help you explore Laravel packages today?