Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
kinetix-forms
happones/kinetix
claude-code
forms
form-fields
validation
precognition
form-layouts
wizard

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.

Install
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.

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.

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

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):

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.

3. Live Validation (Precognition)

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" />

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.

<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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource