mimisk/pinakas

A simple, reusable Laravel table builder package for fast CRUD listings.

Maintainers

Package info

github.com/MimisK13/pinakas

pkg:composer/mimisk/pinakas

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.0.1 2026-04-24 21:32 UTC

This package is auto-updated.

Last update: 2026-04-24 21:33:48 UTC


README

Pinakas table preview

Tests Latest Version on Packagist Total Downloads License: MIT

Pinakas is a simple, reusable Laravel table builder package focused on fast CRUD listings.

Requirements

  • PHP ^8.2
  • Laravel ^11.0, ^12.0, or ^13.0

Installation

Via Composer

composer require mimisk/pinakas

Install JS dependencies for the interactive controls:

npm install alpinejs @tailwindplus/elements

Publish package JS asset:

php artisan vendor:publish --tag=pinakas-assets

Import Alpine and Pinakas JS in your app entrypoint (resources/js/app.js):

import Alpine from 'alpinejs';
import './vendor/pinakas/pinakas';

window.Alpine = Alpine;
Alpine.start();

Rebuild assets:

npm run build

If your app already starts Alpine (for example through Breeze), only import the published Pinakas JS asset once:

import './vendor/pinakas/pinakas';

For Vite dev servers using a custom local domain, make sure HMR uses the same lowercase host as the browser URL. Example:

// vite.config.js
export default defineConfig({
    server: {
        hmr: {
            host: 'packages.test',
        },
        watch: {
            ignored: [
                '**/vendor/**',
                '**/packages/**/vendor/**',
                '**/storage/debugbar/**',
                '**/public/build/**',
            ],
        },
    },
});

Configuration

Publish config:

php artisan vendor:publish --tag=pinakas-config

config/pinakas.php:

'empty_state' => [
    'title' => 'No records found',
    'description' => 'There are no rows available yet.',
    'search_title' => 'No matching results',
    'search_description' => 'Try a different keyword or clear your search.',
],

'search' => [
    'enabled' => false,
    'query_name' => 'search',
    'show_label' => true,
    'label' => 'Search',
    'placeholder' => 'Search...',
    'rounded' => 'rounded-none',
    'debounce_ms' => 350,
    'min_chars' => 3,
    'icon' => 'magnifying-glass',
],

'sorting' => [
    'enabled' => false,
    'query_name' => 'sort',
    'direction_query_name' => 'direction',
    'default_direction' => 'asc',
    'icon_position' => 'right', // left | right
],

'bulk' => [
    'selected_input_name' => 'selected_ids',
    'actions' => [],
],

'ui' => [
    'accent_color' => 'amber-600',
    'table_bordered' => false,
    'table_rounded' => 'rounded-xs',
    'table_striped' => false,
    'table_hoverable' => true,
    'pagination_dropdown_rounded' => 'rounded-none',
    'action_button_rounded' => 'rounded-none',
    'action_dropdown_rounded' => 'rounded-none',
],

'columns' => [
    'date_format' => 'd-m-Y',
    'time_format' => 'H:i',
],

'pagination' => [
    'enabled' => false,
    'default_per_page' => 15,
    'page_name' => 'page',
    'per_page_query_name' => 'per_page',
    'per_page_options' => [10, 25, 50],
    'show_label' => false,
    // Available: default, centered-page-numbers
    'template' => 'default',
],

These are global defaults and can be overridden per table.

Usage

Quick Start

use App\Models\User;
use Mimisk\Pinakas\Actions\ActionGroup;
use Mimisk\Pinakas\Actions\DeleteAction;
use Mimisk\Pinakas\Actions\EditAction;
use Mimisk\Pinakas\Actions\ViewAction;
use Mimisk\Pinakas\Bulk\BulkAction;
use Mimisk\Pinakas\Columns\BadgeColumn;
use Mimisk\Pinakas\Columns\BooleanColumn;
use Mimisk\Pinakas\Columns\Column;
use Mimisk\Pinakas\Columns\DateColumn;
use Mimisk\Pinakas\Columns\TimeColumn;
use Mimisk\Pinakas\Pinakas;

$table = (new Pinakas())
    ->model(User::class)
    ->columns([
        Column::make('Id', 'id')->searchable()->sortable(),
        Column::make('Name', 'name')->searchable()->sortable(),
        Column::make('Email', 'email')->searchable()->sortable(),
        DateColumn::make('Created At', 'created_at')->format('d/m/Y H:i')->sortable(),
    ])
    ->actions([
        ActionGroup::make([
            ViewAction::make(),
            EditAction::make(),
            DeleteAction::make(),
        ]),
    ])
    ->bulkActions([
        BulkAction::make('Delete Selected')
            ->icon('trash')
            ->url('/users/bulk-delete')
            ->method('DELETE')
            ->confirm('Delete selected users?'),
    ])
    ->paginate(10)
    ->searchable()
    ->sortable()
    ->uiAccentColor('amber-600')
    ->bordered(true)
    ->tableRounded('rounded-xs')
    ->paginationDropdownRounded('rounded-none')
    ->actionButtonRounded('rounded-none')
    ->actionDropdownRounded('rounded-none')
    ->perPageOptions([10, 25, 50]);

Rendering The Table View

Pass the table object to your Blade view:

return view('welcome', [
    'table' => $table,
]);

Render the package table in Blade:

@include('pinakas::table', ['table' => $table])

Columns

Define columns with label + model attribute:

->columns([
    Column::make('Id', 'id'),
    Column::make('Name', 'name'),
    Column::make('Email', 'email'),
    Column::make('Created At', 'created_at'),
])

Column alignment:

->columns([
    Column::make('Id', 'id')->align('center'), // header + cells
    Column::make('Name', 'name')->align('left'),
    Column::make('Amount', 'amount')->align('right'),
])

Header alignment (independent from cell alignment):

->columns([
    Column::make('Amount', 'amount')
        ->cellAlign('right')    // cells only
        ->headerAlign('center') // header
])

Header label is optional in make(). If omitted, it is auto-generated from the attribute:

->columns([
    Column::make(attribute: 'created_at'), // "Created At"
    DateColumn::make(attribute: 'email_verified_at'), // "Email Verified At"
])

Date column type:

use Mimisk\Pinakas\Columns\DateColumn;

->columns([
    DateColumn::make('Created At', 'created_at')
        ->format('d/m/Y H:i')
        ->timezone('Europe/Athens')
        ->emptyText('-'),
])

If ->format(...) is omitted, DateColumn uses columns.date_format from config.

Time column type:

use Mimisk\Pinakas\Columns\TimeColumn;

->columns([
    TimeColumn::make('Login Time', 'last_login_at')
        ->format('H:i')
        ->timezone('Europe/Athens')
        ->emptyText('-'),
])

If ->format(...) is omitted, TimeColumn uses columns.time_format from config.

Badge column type:

use Mimisk\Pinakas\Columns\BadgeColumn;

->columns([
    BadgeColumn::make('Status', 'status')
        ->color(fn ($value) => filled($value) ? 'green' : 'gray'),
])

Boolean column type:

use Mimisk\Pinakas\Columns\BooleanColumn;

->columns([
    BooleanColumn::make('Verified', 'is_verified')
        ->labels('Verified', 'Not verified')
        ->colors('green', 'gray'),
])

Actions (Single And Group)

Single actions:

->actions([
    ViewAction::make(),
    EditAction::make(),
    DeleteAction::make(),
])

Grouped actions:

->actions([
    ActionGroup::make([
        ViewAction::make(),
        EditAction::make(),
        DeleteAction::make(),
    ]),
])

Custom action URLs may be generated directly or through named routes:

Action::make('Open')
    ->route('user.show', fn ($row) => ['user' => $row->id])

Destructive actions can show a confirmation modal before submit:

Action::make('Delete')
    ->url('/users/1')
    ->method('DELETE')
    ->confirm('Delete this user?')

DeleteAction::make() uses the Laravel-style *.destroy route name.

Confirmation modals are rendered by pinakas::partials.confirm-modal. Pinakas also ships pinakas::partials.styles for the required x-cloak rule, preventing Alpine dropdown/modal flashes while the page initializes.

Pagination

Enable pagination:

->paginate(10)

Second parameter defines page query key:

->paginate(10, 'users_page')

Third parameter is optional label text (shown above dropdown):

->paginate(10, 'users_page', 'Per Page')

Pagination template override per table:

->paginationTemplate('centered-page-numbers')

You can also pass a direct view name:

->paginationTemplate('my-custom.pagination')

Search

Enable global search:

->searchable()

Custom search query key:

->searchable('q')

Override label / placeholder / icon per table:

->searchLabel('Find User')
->searchPlaceholder('Type name or email')
->searchRounded('rounded-none')
->searchDebounceMs(300)
->searchMinChars(3)
->searchIcon('magnifying-glass') // or 'search', null, or custom view name

Hide label or icon:

->showSearchLabel(false)
->searchIcon(null)

Mark specific columns as searchable:

->columns([
    Column::make('Name', 'name')->searchable(),
    Column::make('Email', 'email')->searchable(),
    Column::make('Created At', 'created_at'), // not searchable
])

If no columns are marked, search is applied to all defined column attributes.

Sorting

Enable sorting support for this table:

->sortable()

Set icon position per table (override config):

->sortIconPosition('right') // or 'left'

Custom sort query keys:

->sortable('sort_by', 'sort_direction')

Mark sortable columns:

->columns([
    Column::make('Id', 'id')->sortable(),
    Column::make('Name', 'name')->sortable(),
    Column::make('Email', 'email')->sortable(),
])

You can combine ->searchable() and ->sortable() safely. Only columns marked with ->sortable() become sortable.

Bulk Actions

Register bulk actions:

use Mimisk\Pinakas\Bulk\BulkAction;

->bulkActions([
    BulkAction::make('Delete Selected')
        ->icon('trash')
        ->url('/users/bulk-delete')
        ->method('DELETE')
        ->confirm('Delete selected users?'),
])

Customize selected IDs input name:

->bulkSelectedInputName('ids')

Controller endpoint example:

public function bulkDelete(Request $request)
{
    $ids = $request->input('selected_ids', []);
    User::query()->whereIn('id', $ids)->delete();

    return back();
}

UI Settings

Override global accent color per table:

->uiAccentColor('amber-600')

You may also pass CSS color values:

->uiAccentColor('#d97706')

Control outer table border per table:

->bordered(true)  // show border
->bordered(false) // hide border

Control table rounded class per table:

->tableRounded('rounded-xs')
->tableRounded('rounded-lg')
->tableRounded('rounded-none')

Control striped rows per table:

->striped()       // ON
->striped(false)  // OFF

Control hoverable rows per table:

->hoverable()       // ON
->hoverable(false)  // OFF

Control rounded classes for controls:

->paginationDropdownRounded('rounded-none')
->actionButtonRounded('rounded-none')
->actionDropdownRounded('rounded-none')

Per-Page Selector

Per page dropdown appears automatically when pagination is enabled. No label is shown by default.

el-select is powered by @tailwindplus/elements (npm).

->perPageOptions([10, 25, 50])

Custom per-page query key:

->perPageOptions([10, 25, 50], 'users_per_page')

Label visibility is controlled globally by pagination.show_label. If show_label = true and no custom text is passed, default text is Per page.

Loading State

The table includes a built-in loading overlay (spinner + reduced opacity) while form submissions and link navigations are in progress. The UI classes include dark mode variants for table, controls, and loading state.

Empty States

When the table has no data:

->emptyState('No users yet', 'Create your first user to get started.')

When search has no matches:

->searchEmptyState('No users found', 'Try another term or clear search.')

Full Controller Example

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Mimisk\Pinakas\Actions\ActionGroup;
use Mimisk\Pinakas\Actions\DeleteAction;
use Mimisk\Pinakas\Actions\EditAction;
use Mimisk\Pinakas\Actions\ViewAction;
use Mimisk\Pinakas\Bulk\BulkAction;
use Mimisk\Pinakas\Columns\Column;
use Mimisk\Pinakas\Pinakas;

class TestController extends Controller
{
    public function showTable()
    {
        $table = (new Pinakas())
            ->model(User::class)
            ->columns([
                Column::make('Id', 'id')->searchable()->sortable(),
                Column::make('Name', 'name')->searchable()->sortable(),
                Column::make('Email', 'email')->searchable()->sortable(),
                Column::make('Created At', 'created_at'),
            ])
            ->actions([
                ActionGroup::make([
                    ViewAction::make(),
                    EditAction::make(),
                    DeleteAction::make(),
                ]),
            ])
            ->bulkActions([
                BulkAction::make('Delete Selected')
                    ->icon('trash')
                    ->url('/users/bulk-delete')
                    ->method('DELETE')
                    ->confirm('Delete selected users?'),
            ])
            ->paginate(10)
            ->searchable()
            ->sortable()
            ->perPageOptions([10, 25, 50]);

        return view('welcome', [
            'table' => $table,
        ]);
    }
}

Notes

Route naming convention used by default actions:

  • user.show
  • user.edit
  • user.destroy

These are inferred from each row model class (example: App\Models\User -> user.*).

Change log

Please see the CHANGELOG for more information on what has changed recently.

Testing

composer install
composer test

Static analysis is handled by Larastan:

composer analyse

The GitHub workflow runs both Pest and Larastan against the supported Laravel matrix.

Security

If you discover any security related issues, please email mimisk88@gmail.com instead of using the issue tracker.

Credits

License

MIT. Please see the LICENSE file for more information.