Migrates a v6 Theme class to the v7 architecture: a tiny struct() plus per-section token methods (layout/header/table/footer/cols/tabs + filter/editable/toggleable) with parentTheme inheritance
php artisan boost:add-skill power-components/livewire-powergrid
Save this content to: .claude/skills/upgrade-theme-v6-to-v7/SKILL.md
## What I do
- Read a legacy v6 theme class (Bootstrap, DaisyUI, a custom third-party theme) from GitHub or local disk.
- Create a v7 theme class with a tiny `struct()` (base view only) plus the per-section token methods (`layout()`, `header()`, `table()`, `footer()`, `cols()`, `tabs()`, `filter()`, `editable()`, `toggleable()`).
- Map every legacy v6 array method (`table()`, `header()`, `checkbox()`, `searchBox()`, `filter*()`, ...) onto the correct v7 section and token key.
- Set `parentTheme = Tailwind::class` so the migrated theme only declares what actually differs.
This skill is the **canonical owner** of the complete v6 -> v7 mapping table, which lives in [`REFERENCE.md`](REFERENCE.md).
## When to use me
- Porting a legacy PowerGrid v6 theme to the v7 architecture.
- Upgrading a custom theme that still returns arrays from `table()`, `checkbox()`, `footer()`, etc. with the old (flat/renamed) keys.
## How to use me
```
Use the 'upgrade-theme-v6-to-v7' skill to migrate the v6 Bootstrap theme at
https://raw.githubusercontent.com/Power-Components/livewire-powergrid/6.x/src/Themes/Bootstrap5.php
```
## The good news: v7 is closer to v6 than you think
v7 did **not** collapse everything into one builder. Like v6, a theme is still a
set of **per-section methods that return arrays**. The migration is mostly
**renaming and re-nesting keys** and turning on inheritance. Four things change:
1. **`struct()` is tiny.** It only sets the base view — it does **not** carry the token tree:
```php
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string { return 'livewire-powergrid::components.themes.bootstrap5'; }
```
2. **Token groups are per-section methods** returning `['<group>' => [...]]`. The group list is in `Theme::themeTokenMethods()`: `layout`, `header`, `table`, `footer`, `cols`, `tabs`, `filter`, `editable`, `toggleable`. Write each as a **plain nested array** (closest to v6) or with the fluent `$this->section('<group>', fn (...) => ...)` helper.
3. **The hierarchy is re-nested and renamed:**
- `checkbox` and `radio` move **under** `table` (`table.checkbox.*`, `table.radio.*`).
- `searchBox` moves **under** `header` (`header.search_box.*`).
- `footer` gains a nested `layout`; the old `footer_with_pagination` becomes `pagination`.
- `table.header.*` / `table.body.*` flatten to `table.layout.*`; `thAction` → `th_actions`, `tdActionsContainer` → `td_actions`.
4. **Inheritance replaces boilerplate.** Set `protected ?string $parentTheme = Tailwind::class;` and declare **only the sections that differ** from Tailwind. `editable()`, `toggleable()`, and `filter()` were also reworked:
- `editable()` keeps `clickable` / `input` / `error` **classes only**. Discard `view` — the plugin blade is `powergrid-plugins::Editable.index`.
- `toggleable()` is now **five CSS color tokens** (`colorOn`, `colorOff`, `colorOnDark`, `colorOffDark`, `knobOn`) set via `->fill([...])`. The old `view/base/label/input/role` keys are gone.
- `filter()` adds a `label`, a global `input`, and `dropdown`/`flyout` groups. Discard `filter.multi_select.view` — markup is `<x-livewire-powergrid::inputs.select>`.
> Token keys are snake_cased when built, so the fluent method `->thActions()` (or array key) is stored as the token `table.layout.th_actions`. You write camelCase in the fluent form; the resolved token path is snake_case. v7 also adds a `tabs` group (status tabs) with no v6 equivalent.
Full builder-class method lists, the complete mapping table, and a full worked example are in [`REFERENCE.md`](REFERENCE.md).
## Migration workflow
1. **Read the v7 base.** Open `src/Themes/Tailwind.php` — its section methods are the source of truth for the token surface. Also skim `DaisyUI.php` (a token-only subclass that ships **zero blades**) as the model to imitate. Do not rely on a memorized count -- diff against these files (see Validation).
2. **Read the v6 theme.** Collect the CSS classes from every method: `table()`, `cols()`, `footer()`, `checkbox()`, `radio()`, `editable()`, `toggleable()`, `searchBox()`, and each `filter*()`.
3. **Create `src/Themes/[ThemeName].php`** extending `\PowerComponents\LivewirePowerGrid\Themes\Theme`, `use ...\Themes\Components;`, with `protected ?string $parentTheme = Tailwind::class;` and a tiny `struct()` + `baseView()`.
4. **Add the section methods** you need (`layout()`, `header()`, `table()`, `cols()`, `footer()`, `tabs()`), translating each v6 value with the mapping table in `REFERENCE.md`. Declare only sections that differ from Tailwind.
5. **Add `editable()`, `toggleable()`, `filter()`** as separate methods (see the mini-example below).
6. **Validate** the token set against `Tailwind` (below), then update any references to the old theme class and run the tests.
## Worked mini-example (v6 Bootstrap5 -> v7)
The v7 class below shows the tiny `struct()`, `parentTheme`, and the structural
moves. The complete class (with the full `filter()`) is in [`REFERENCE.md`](REFERENCE.md).
```php
<?php
namespace PowerComponents\LivewirePowerGrid\Themes;
use PowerComponents\LivewirePowerGrid\Themes\Components;
class Bootstrap5 extends Theme
{
// Inherit Tailwind for everything not declared below.
protected ?string $parentTheme = Tailwind::class;
// struct() only carries the base view.
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string
{
return 'livewire-powergrid::components.themes.bootstrap5';
}
/** @return array<string, mixed> */
public function header(): array
{
return $this->section('header', fn (Components\Header $header) => $header
->view('header')
->layout(fn (Components\Layout $layout) => $layout
->container('d-flex justify-content-between align-items-center mb-3')
->subContainer('d-flex gap-1')
->actionsContainer('d-flex align-items-center flex-wrap')
->actions('btn btn-outline-secondary btn-sm') // v6 table.layout.actions
)
->searchBox(fn (Components\SearchBox $searchBox) => $searchBox // moved under header
->view('header.search')
->input('form-control') // v6 searchBox.input
->iconClose('bi bi-x') // v6 searchBox.iconClose
->iconSearch('bi bi-search') // v6 searchBox.iconSearch
)
);
}
/** @return array<string, mixed> */
public function table(): array
{
return $this->section('table', fn (Components\Table $table) => $table
->layout(fn (Components\Layout $layout) => $layout
->container('my-0') // v6 table.layout.container
->table('table table-hover table-striped') // v6 table.layout.table
->th('fw-bold text-secondary') // v6 table.header.th
->thActions('text-center') // v6 table.header.thAction (renamed)
->td('align-middle text-nowrap') // v6 table.body.td
->tdActions('text-end') // v6 table.body.tdActionsContainer (renamed)
)
->checkbox(fn (Components\Checkbox $checkbox) => $checkbox // moved under table
->th('text-center') // v6 checkbox.th
->base('form-check') // v6 checkbox.base
->label('form-check-label') // v6 checkbox.label
->input('form-check-input') // v6 checkbox.input
)
->radio(fn (Components\Radio $radio) => $radio // moved under table
->th('text-center')
->base('form-check')
->label('form-check-label')
->input('form-check-input')
)
);
}
/** @return array<string, mixed> */
public function footer(): array
{
return $this->section('footer', fn (Components\Footer $footer) => $footer
->view('footer') // v6 footer.view (alias only)
->layout(fn (Components\Layout $layout) => $layout // v6 footer keys nested under layout
->container('d-flex justify-content-between') // v6 footer.footer
->select('form-select') // v6 footer.select
)
->pagination('pagination') // v6 footer.footer_with_pagination
);
}
/** @return array<string, mixed> */
public function editable(): array
{
// Classes only. Plugin blade is powergrid-plugins::Editable.index — no ->view().
return [
'editable' => (new Components\Component())
->clickable('py-2') // NEW in v7
->input('form-control')
->error('invalid-feedback d-block') // NEW in v7
->toArray(),
];
}
/** @return array<string, mixed> */
public function toggleable(): array
{
// v7 toggleable is a pure CSS switch: five color tokens, no view/base/label/input/role.
return [
'toggleable' => (new Components\Component())
->fill([
'colorOn' => 'var(--bs-success, #198754)',
'colorOff' => 'var(--bs-gray-300, #dee2e6)',
'colorOnDark' => 'var(--bs-success, #198754)',
'colorOffDark' => 'var(--bs-gray-600, #6c757d)',
'knobOn' => 'var(--bs-white, #ffffff)',
])
->toArray(),
];
}
/** @return array<string, mixed> */
public function filter(): array
{
// Abbreviated -- the full filter() (all filter types + dropdown/flyout) is in REFERENCE.md.
return [
'filter' => (new Components\Filter())
->label('form-label')
->boolean(fn (Components\Component $c) => $c
->view('livewire-powergrid::components.themes.bootstrap5.filters.boolean')
->select('form-select')
)
->inputText(fn (Components\Component $c) => $c
->view('livewire-powergrid::components.themes.bootstrap5.filters.input-text')
->select('form-select')
->input('form-control')
)
->input('form-control') // NEW in v7 - global input
->toArray(),
];
}
}
```
Any section method can equally be written as a **plain nested array** instead of
`section()`, which is often the smallest diff from v6, e.g.
`public function cols(): array { return ['cols' => ['div' => 'd-flex align-items-center gap-1']]; }`.
## Prefer tokens over new blades
DaisyUI ships **zero** Blade files — it renders entirely through Tailwind's
markup plus its own tokens, inheriting views via `parentTheme` + the
view-resolution fallback in `Theme::doResolveView()`. When migrating, only add a
Blade file (and a `->view()` token) for markup that is genuinely different for
your framework. Everything else inherits Tailwind's blade automatically.
## Validation
This is a package with no artisan console, so there is no interactive REPL to poke tokens in. Verify the token set with a Pest test that diffs the migrated theme's resolved tokens against the base `Tailwind` theme (drop it in `tests/`, run with `./vendor/bin/pest`):
```php
<?php
use Illuminate\Support\Arr;
use PowerComponents\LivewirePowerGrid\Themes\Bootstrap5;
use PowerComponents\LivewirePowerGrid\Themes\Tailwind;
it('defines every token the base Tailwind theme defines', function () {
$base = array_keys(Arr::dot((new Tailwind())->resolveTokens()));
$migrated = array_keys(Arr::dot((new Bootstrap5())->resolveTokens()));
// Keys present in the base but missing from the migrated theme:
expect(array_diff($base, $migrated))->toBe([]);
});
```
Use `resolveTokens()` (not `struct()`) so the diff covers the section methods,
`editable()`/`toggleable()`/`filter()`, and `parentTheme` inheritance together.
Because `parentTheme = Tailwind::class` fills every undeclared token, the diff
will already be empty once inheritance is set — the assertion mainly catches a
missing/misspelled key you tried to override. Fill any reported gap with the
appropriate class (or `''`).
Run the suite:
```bash
composer test # runs ./vendor/bin/pest --compact (see composer.json scripts)
./vendor/bin/pest --filter=Theme
```
## Checklist
- [ ] `struct()` returns `ThemeBuilder::make(...)->baseView(...)` only -- the token tree lives in section methods.
- [ ] `protected ?string $parentTheme = Tailwind::class;` is declared, so only differing sections are overridden.
- [ ] Each token group is its own method (`layout`/`header`/`table`/`footer`/`cols`/`tabs`/`filter`/`editable`/`toggleable`), returning `['<group>' => [...]]` (array or via `section()`).
- [ ] `checkbox` / `radio` nested under `table`; `searchBox` nested under `header`; `footer` uses a nested `layout` + `pagination()`; `table.header.*`/`table.body.*` flattened to `table.layout.*`.
- [ ] Separate `editable()` (clickable/input/error — **no** `->view()`), `toggleable()` (five color tokens — **no** view), and `filter()` (label + global input; **no** `multi_select.view`).
- [ ] A Blade file is added only for genuinely different markup; otherwise Tailwind's view is inherited.
- [ ] `baseView()` points at `livewire-powergrid::components.themes.<name>`.
- [ ] No `public string $name` property -- `Theme::name()` derives the name from the class name.
- [ ] Token set verified against `Tailwind` via the Pest diff above (not a memorized count).
- [ ] Tests pass; no references to the old theme class remain.
struct() (base view only) plus the per-section token methods (layout(), header(), table(), footer(), cols(), tabs(), filter(), editable(), toggleable()).table(), header(), checkbox(), searchBox(), filter*(), ...) onto the correct v7 section and token key.parentTheme = Tailwind::class so the migrated theme only declares what actually differs.This skill is the canonical owner of the complete v6 -> v7 mapping table, which lives in REFERENCE.md.
table(), checkbox(), footer(), etc. with the old (flat/renamed) keys.Use the 'upgrade-theme-v6-to-v7' skill to migrate the v6 Bootstrap theme at
https://raw.githubusercontent.com/Power-Components/livewire-powergrid/6.x/src/Themes/Bootstrap5.php
v7 did not collapse everything into one builder. Like v6, a theme is still a set of per-section methods that return arrays. The migration is mostly renaming and re-nesting keys and turning on inheritance. Four things change:
struct() is tiny. It only sets the base view — it does not carry the token tree:
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string { return 'livewire-powergrid::components.themes.bootstrap5'; }
['<group>' => [...]]. The group list is in Theme::themeTokenMethods(): layout, header, table, footer, cols, tabs, filter, editable, toggleable. Write each as a plain nested array (closest to v6) or with the fluent $this->section('<group>', fn (...) => ...) helper.checkbox and radio move under table (table.checkbox.*, table.radio.*).searchBox moves under header (header.search_box.*).footer gains a nested layout; the old footer_with_pagination becomes pagination.table.header.* / table.body.* flatten to table.layout.*; thAction → th_actions, tdActionsContainer → td_actions.protected ?string $parentTheme = Tailwind::class; and declare only the sections that differ from Tailwind. editable(), toggleable(), and filter() were also reworked:
editable() keeps clickable / input / error classes only. Discard view — the plugin blade is powergrid-plugins::Editable.index.toggleable() is now five CSS color tokens (colorOn, colorOff, colorOnDark, colorOffDark, knobOn) set via ->fill([...]). The old view/base/label/input/role keys are gone.filter() adds a label, a global input, and dropdown/flyout groups. Discard filter.multi_select.view — markup is <x-livewire-powergrid::inputs.select>.Token keys are snake_cased when built, so the fluent method
->thActions()(or array key) is stored as the tokentable.layout.th_actions. You write camelCase in the fluent form; the resolved token path is snake_case. v7 also adds atabsgroup (status tabs) with no v6 equivalent.
Full builder-class method lists, the complete mapping table, and a full worked example are in REFERENCE.md.
src/Themes/Tailwind.php — its section methods are the source of truth for the token surface. Also skim DaisyUI.php (a token-only subclass that ships zero blades) as the model to imitate. Do not rely on a memorized count -- diff against these files (see Validation).table(), cols(), footer(), checkbox(), radio(), editable(), toggleable(), searchBox(), and each filter*().src/Themes/[ThemeName].php extending \PowerComponents\LivewirePowerGrid\Themes\Theme, use ...\Themes\Components;, with protected ?string $parentTheme = Tailwind::class; and a tiny struct() + baseView().layout(), header(), table(), cols(), footer(), tabs()), translating each v6 value with the mapping table in REFERENCE.md. Declare only sections that differ from Tailwind.editable(), toggleable(), filter() as separate methods (see the mini-example below).Tailwind (below), then update any references to the old theme class and run the tests.The v7 class below shows the tiny struct(), parentTheme, and the structural
moves. The complete class (with the full filter()) is in REFERENCE.md.
<?php
namespace PowerComponents\LivewirePowerGrid\Themes;
use PowerComponents\LivewirePowerGrid\Themes\Components;
class Bootstrap5 extends Theme
{
// Inherit Tailwind for everything not declared below.
protected ?string $parentTheme = Tailwind::class;
// struct() only carries the base view.
public function struct(): Components\ThemeBuilder
{
return Components\ThemeBuilder::make($this->name())->baseView($this->baseView());
}
protected function baseView(): string
{
return 'livewire-powergrid::components.themes.bootstrap5';
}
/** @return array<string, mixed> */
public function header(): array
{
return $this->section('header', fn (Components\Header $header) => $header
->view('header')
->layout(fn (Components\Layout $layout) => $layout
->container('d-flex justify-content-between align-items-center mb-3')
->subContainer('d-flex gap-1')
->actionsContainer('d-flex align-items-center flex-wrap')
->actions('btn btn-outline-secondary btn-sm') // v6 table.layout.actions
)
->searchBox(fn (Components\SearchBox $searchBox) => $searchBox // moved under header
->view('header.search')
->input('form-control') // v6 searchBox.input
->iconClose('bi bi-x') // v6 searchBox.iconClose
->iconSearch('bi bi-search') // v6 searchBox.iconSearch
)
);
}
/** @return array<string, mixed> */
public function table(): array
{
return $this->section('table', fn (Components\Table $table) => $table
->layout(fn (Components\Layout $layout) => $layout
->container('my-0') // v6 table.layout.container
->table('table table-hover table-striped') // v6 table.layout.table
->th('fw-bold text-secondary') // v6 table.header.th
->thActions('text-center') // v6 table.header.thAction (renamed)
->td('align-middle text-nowrap') // v6 table.body.td
->tdActions('text-end') // v6 table.body.tdActionsContainer (renamed)
)
->checkbox(fn (Components\Checkbox $checkbox) => $checkbox // moved under table
->th('text-center') // v6 checkbox.th
->base('form-check') // v6 checkbox.base
->label('form-check-label') // v6 checkbox.label
->input('form-check-input') // v6 checkbox.input
)
->radio(fn (Components\Radio $radio) => $radio // moved under table
->th('text-center')
->base('form-check')
->label('form-check-label')
->input('form-check-input')
)
);
}
/** @return array<string, mixed> */
public function footer(): array
{
return $this->section('footer', fn (Components\Footer $footer) => $footer
->view('footer') // v6 footer.view (alias only)
->layout(fn (Components\Layout $layout) => $layout // v6 footer keys nested under layout
->container('d-flex justify-content-between') // v6 footer.footer
->select('form-select') // v6 footer.select
)
->pagination('pagination') // v6 footer.footer_with_pagination
);
}
/** @return array<string, mixed> */
public function editable(): array
{
// Classes only. Plugin blade is powergrid-plugins::Editable.index — no ->view().
return [
'editable' => (new Components\Component())
->clickable('py-2') // NEW in v7
->input('form-control')
->error('invalid-feedback d-block') // NEW in v7
->toArray(),
];
}
/** @return array<string, mixed> */
public function toggleable(): array
{
// v7 toggleable is a pure CSS switch: five color tokens, no view/base/label/input/role.
return [
'toggleable' => (new Components\Component())
->fill([
'colorOn' => 'var(--bs-success, #198754)',
'colorOff' => 'var(--bs-gray-300, #dee2e6)',
'colorOnDark' => 'var(--bs-success, #198754)',
'colorOffDark' => 'var(--bs-gray-600, #6c757d)',
'knobOn' => 'var(--bs-white, #ffffff)',
])
->toArray(),
];
}
/** @return array<string, mixed> */
public function filter(): array
{
// Abbreviated -- the full filter() (all filter types + dropdown/flyout) is in REFERENCE.md.
return [
'filter' => (new Components\Filter())
->label('form-label')
->boolean(fn (Components\Component $c) => $c
->view('livewire-powergrid::components.themes.bootstrap5.filters.boolean')
->select('form-select')
)
->inputText(fn (Components\Component $c) => $c
->view('livewire-powergrid::components.themes.bootstrap5.filters.input-text')
->select('form-select')
->input('form-control')
)
->input('form-control') // NEW in v7 - global input
->toArray(),
];
}
}
Any section method can equally be written as a plain nested array instead of
section(), which is often the smallest diff from v6, e.g.
public function cols(): array { return ['cols' => ['div' => 'd-flex align-items-center gap-1']]; }.
DaisyUI ships zero Blade files — it renders entirely through Tailwind's
markup plus its own tokens, inheriting views via parentTheme + the
view-resolution fallback in Theme::doResolveView(). When migrating, only add a
Blade file (and a ->view() token) for markup that is genuinely different for
your framework. Everything else inherits Tailwind's blade automatically.
This is a package with no artisan console, so there is no interactive REPL to poke tokens in. Verify the token set with a Pest test that diffs the migrated theme's resolved tokens against the base Tailwind theme (drop it in tests/, run with ./vendor/bin/pest):
<?php
use Illuminate\Support\Arr;
use PowerComponents\LivewirePowerGrid\Themes\Bootstrap5;
use PowerComponents\LivewirePowerGrid\Themes\Tailwind;
it('defines every token the base Tailwind theme defines', function () {
$base = array_keys(Arr::dot((new Tailwind())->resolveTokens()));
$migrated = array_keys(Arr::dot((new Bootstrap5())->resolveTokens()));
// Keys present in the base but missing from the migrated theme:
expect(array_diff($base, $migrated))->toBe([]);
});
Use resolveTokens() (not struct()) so the diff covers the section methods,
editable()/toggleable()/filter(), and parentTheme inheritance together.
Because parentTheme = Tailwind::class fills every undeclared token, the diff
will already be empty once inheritance is set — the assertion mainly catches a
missing/misspelled key you tried to override. Fill any reported gap with the
appropriate class (or '').
Run the suite:
composer test # runs ./vendor/bin/pest --compact (see composer.json scripts)
./vendor/bin/pest --filter=Theme
struct() returns ThemeBuilder::make(...)->baseView(...) only -- the token tree lives in section methods.protected ?string $parentTheme = Tailwind::class; is declared, so only differing sections are overridden.layout/header/table/footer/cols/tabs/filter/editable/toggleable), returning ['<group>' => [...]] (array or via section()).checkbox / radio nested under table; searchBox nested under header; footer uses a nested layout + pagination(); table.header.*/table.body.* flattened to table.layout.*.editable() (clickable/input/error — no ->view()), toggleable() (five color tokens — no view), and filter() (label + global input; no multi_select.view).baseView() points at livewire-powergrid::components.themes.<name>.public string $name property -- Theme::name() derives the name from the class name.Tailwind via the Pest diff above (not a memorized count).How can I help you explore Laravel packages today?