Drag-and-drop board over an Eloquent model: records grouped into columns by a status attribute; dragging a card persists the new status. Activates when building a Kanban/status board.
php artisan boost:add-skill happones/kinetix
Save this content to: .claude/skills/kinetix-kanban/SKILL.md
# Kinetix Kanban Development
## When to Apply
Activate this skill when:
- Building a status board / Kanban over a model (tasks, leads, tickets…).
- Rendering `<KinetixKanban>` or letting users drag cards between statuses.
## Documentation
For full details, reference `docs/kanban.md` (published at https://happones.github.io/kinetix/kanban).
## Backend (server-driven, like Tables)
```php
use Happones\Kinetix\Kanban\Kanban;
$board = Kanban::make(Task::query())
->statusColumn('status')
->statuses([
'todo' => 'To Do',
'doing' => ['label' => 'In Progress', 'color' => '#3b82f6'],
'done' => ['label' => 'Done', 'color' => '#22c55e'],
])
->cardTitle('title')
->cardDescription(fn (Task $t) => $t->assignee?->name)
->query(fn ($q) => $q->where('archived', false));
return Inertia::render('Tasks/Board', ['board' => $board->toData()]);
```
No migration or config flag. `toData()` bakes a signed descriptor; the
`POST {prefix}/tables/kanban-move` endpoint (always registered) decrypts it and
only writes the declared status column to one of the declared statuses.
### Enum status columns
A `statusColumn` cast to a PHP enum works natively: grouping stringifies
`BackedEnum` → backing value / `UnitEnum` → case name, and the move re-casts the
plain string. Use the backing values as the `statuses()` keys.
### Record-level authorization (multi-tenant — use at least one)
- **Policy (automatic)**: when the model has a registered policy, every move is
authorized against it — `update` by default, or `->authorizeMove('moveCard')`
for a specific ability. Denied → 403 + card snaps back.
- **`->moveScope(['team_id' => $teamId])`**: column=>value constraints evaluated
in the request, sealed in the encrypted descriptor and enforced on the move
lookup (outside → 404). Without one of these, any authenticated user who can
guess a record id can move records of other tenants.
## Frontend
```vue
<KinetixKanban
:kanban="board"
@card-click="(card, columnKey) => …"
/>
```
Native HTML5 drag-and-drop (no extra dependency), with a long-press touch
fallback on mobile (floating clone follows the finger; edge auto-scroll).
While dragging, the source card dims, the hovered column highlights and shows
a dashed ghost placeholder previewing where the card will land; cards
FLIP-animate into place (respects `prefers-reduced-motion`). Moves are
optimistic (revert + toast on failure) and trigger a `router.reload()` on
success. Keyboard: left/right arrows move a focused card between columns;
Enter fires `card-click`. i18n `kanban_*` (en/es/fr/pt/zh/ja/ru).
`->reorderable('sort_order')` keeps a manual order within each column:
- cards show in that order, then by key;
- a drop lands at the slot under the pointer or finger, with the ghost at that slot;
- dragging within a column reorders it, and up/down arrows move a card one place;
- the move posts the destination column's `order`, and status + positions save
in one transaction (write-checked, all-or-nothing);
- positions are traded via the shared `Support\ManualOrder` (same as table
reorder), so cards a narrower board hides keep theirs; a card from another
column takes a free position between its neighbours (else after the last);
only cards whose position changes are written and write-checked; an
unnumbered column is numbered once within `moveScope()`.
## Adding & editing cards (CRUD wiring)
The board only moves cards; CRUD is regular page wiring. Preferred: an
in-page modal — a `KinetixPageHeader` action with `->dispatch('task-create')`,
the page listens for `kinetix:task-create` and opens a `KinetixModal` hosting
`<KinetixForm :form="taskForm" flat @submit="submit" />` (**always pass `flat`
in modals** — the panel is the surface; Sections must not nest a card inside
it); `@card-click` opens the same modal for edits. Controllers persist,
flash `KinetixFlash::success(__('kinetix.record_created'))`
(`record_updated`/`record_deleted` for the other verbs) and `return back()`.
Alternative: dedicated pages via `router.visit()` on `card-click`, with the
page controllers flashing the same way before they redirect. Full example: docs/kanban.md
"Adding & editing cards".
Activate this skill when:
<KinetixKanban> or letting users drag cards between statuses.For full details, reference docs/kanban.md (published at https://happones.github.io/kinetix/kanban).
use Happones\Kinetix\Kanban\Kanban;
$board = Kanban::make(Task::query())
->statusColumn('status')
->statuses([
'todo' => 'To Do',
'doing' => ['label' => 'In Progress', 'color' => '#3b82f6'],
'done' => ['label' => 'Done', 'color' => '#22c55e'],
])
->cardTitle('title')
->cardDescription(fn (Task $t) => $t->assignee?->name)
->query(fn ($q) => $q->where('archived', false));
return Inertia::render('Tasks/Board', ['board' => $board->toData()]);
No migration or config flag. toData() bakes a signed descriptor; the
POST {prefix}/tables/kanban-move endpoint (always registered) decrypts it and
only writes the declared status column to one of the declared statuses.
A statusColumn cast to a PHP enum works natively: grouping stringifies
BackedEnum → backing value / UnitEnum → case name, and the move re-casts the
plain string. Use the backing values as the statuses() keys.
update by default, or ->authorizeMove('moveCard')
for a specific ability. Denied → 403 + card snaps back.->moveScope(['team_id' => $teamId]): column=>value constraints evaluated
in the request, sealed in the encrypted descriptor and enforced on the move
lookup (outside → 404). Without one of these, any authenticated user who can
guess a record id can move records of other tenants.<KinetixKanban
:kanban="board"
@card-click="(card, columnKey) => …"
/>
Native HTML5 drag-and-drop (no extra dependency), with a long-press touch
fallback on mobile (floating clone follows the finger; edge auto-scroll).
While dragging, the source card dims, the hovered column highlights and shows
a dashed ghost placeholder previewing where the card will land; cards
FLIP-animate into place (respects prefers-reduced-motion). Moves are
optimistic (revert + toast on failure) and trigger a router.reload() on
success. Keyboard: left/right arrows move a focused card between columns;
Enter fires card-click. i18n kanban_* (en/es/fr/pt/zh/ja/ru).
->reorderable('sort_order') keeps a manual order within each column:
order, and status + positions save
in one transaction (write-checked, all-or-nothing);Support\ManualOrder (same as table
reorder), so cards a narrower board hides keep theirs; a card from another
column takes a free position between its neighbours (else after the last);
only cards whose position changes are written and write-checked; an
unnumbered column is numbered once within moveScope().The board only moves cards; CRUD is regular page wiring. Preferred: an
in-page modal — a KinetixPageHeader action with ->dispatch('task-create'),
the page listens for kinetix:task-create and opens a KinetixModal hosting
<KinetixForm :form="taskForm" flat @submit="submit" /> (always pass flat
in modals — the panel is the surface; Sections must not nest a card inside
it); @card-click opens the same modal for edits. Controllers persist,
flash KinetixFlash::success(__('kinetix.record_created'))
(record_updated/record_deleted for the other verbs) and return back().
Alternative: dedicated pages via router.visit() on card-click, with the
page controllers flashing the same way before they redirect. Full example: docs/kanban.md
"Adding & editing cards".
How can I help you explore Laravel packages today?