Projects
A workspace projects panel — a status-grouped kanban board behind a By status / Active / Completed / All filter row.
Preview
{{--
Props:
- title: section heading text.
- projects: [['id', 'title', 'status' => backlog|todo|in-progress|done,
'icon' => folder|file|doc, 'assignee' => ['name'], 'start', 'end', 'amount']].
Every card is grouped into its status column by `status`; null falls
back to eight sample projects across the four statuses.
- filter: the view pill shown first — status|active|completed|all.
status shows all four columns; active hides Completed; completed shows
only Completed; all drops the status grouping and lists every project
in one horizontal, scrollable lane instead.
Switching pills is client-side (Alpine) — no request round-trip.
--}}
@props([
'title' => 'Projects',
'projects' => null,
'filter' => 'status',
])
@php
$projects ??= [
['id' => 'website-redesign', 'title' => 'Website Redesign', 'status' => 'backlog', 'icon' => 'folder', 'assignee' => ['name' => 'Mara Lindgren'], 'start' => 'Dec 2, 2026', 'end' => 'Dec 12, 2026', 'amount' => '$3,200.00'],
['id' => 'onboarding-flow', 'title' => 'Onboarding Flow', 'status' => 'backlog', 'icon' => 'folder', 'assignee' => ['name' => 'Théo Dubois'], 'start' => 'Jan 5, 2027', 'end' => 'Jan 20, 2027', 'amount' => '$1,800.00'],
['id' => 'mobile-banking-app', 'title' => 'Mobile Banking App', 'status' => 'todo', 'icon' => 'folder', 'assignee' => ['name' => 'Priya Nair'], 'start' => 'Nov 18, 2026', 'end' => 'Dec 2, 2026', 'amount' => '$4,800.00'],
['id' => 'support-portal', 'title' => 'Support Portal', 'status' => 'todo', 'icon' => 'folder', 'assignee' => ['name' => 'Owen Baxter'], 'start' => 'Nov 24, 2026', 'end' => 'Dec 8, 2026', 'amount' => '$2,100.00'],
['id' => 'crm-dashboard', 'title' => 'CRM Dashboard', 'status' => 'in-progress', 'icon' => 'folder', 'assignee' => ['name' => 'Sofia Kowalska'], 'start' => 'Nov 10, 2026', 'end' => 'Nov 24, 2026', 'amount' => '$2,750.00'],
['id' => 'analytics-suite', 'title' => 'Analytics Suite', 'status' => 'in-progress', 'icon' => 'folder', 'assignee' => ['name' => 'Diego Santos'], 'start' => 'Oct 30, 2026', 'end' => 'Dec 1, 2026', 'amount' => '$6,400.00'],
['id' => 'ecommerce-platform', 'title' => 'E-commerce Platform', 'status' => 'done', 'icon' => 'folder', 'assignee' => ['name' => 'Amara Chen'], 'start' => 'Oct 28, 2026', 'end' => 'Nov 15, 2026', 'amount' => '$5,500.00'],
['id' => 'marketing-site', 'title' => 'Marketing Site', 'status' => 'done', 'icon' => 'folder', 'assignee' => ['name' => 'Noah Fischer'], 'start' => 'Oct 1, 2026', 'end' => 'Oct 20, 2026', 'amount' => '$3,000.00'],
];
$filter = in_array($filter, ['status', 'active', 'completed', 'all'], true) ? $filter : 'status';
$statusTitles = [
'backlog' => __('Not started'),
'todo' => __('Started'),
'in-progress' => __('In progress'),
'done' => __('Completed'),
];
$byStatus = collect($projects)->groupBy('status');
$statusColumns = collect($statusTitles)
->map(fn ($label, $status) => [
'id' => $status,
'title' => $label,
'status' => $status,
'cards' => ($byStatus->get($status) ?? collect())->values()->all(),
])
->values()
->all();
$activeColumns = collect($statusColumns)->reject(fn ($column) => $column['status'] === 'done')->values()->all();
$completedColumns = collect($statusColumns)->filter(fn ($column) => $column['status'] === 'done')->values()->all();
// "All" drops the status grouping entirely: every project in one
// horizontal lane (kanban's own responsive scroll, never the page).
$allColumns = [[
'id' => 'all',
'title' => __('All projects'),
'cards' => collect($projects)->values()->all(),
]];
@endphp
<x-ui.section data-slot="projects-board" space="lg" x-data="{ filter: {{ Js::from($filter) }} }" {{ $attributes }}>
<x-ui.container gutter="sm">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-2">
<h2 class="text-xl font-semibold tracking-tight text-foreground sm:text-2xl">{{ __($title) }}</h2>
<x-ui.badge variant="secondary" size="sm" :aria-label="__(':count projects', ['count' => count($projects)])">{{ count($projects) }}</x-ui.badge>
</div>
<x-ui.segmented-nav as="div" size="sm" :label="__('Filter projects')" class="flex-wrap">
<x-ui.segmented-nav.item x-active="filter === 'status'" @click="filter = 'status'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="5" height="16" rx="1" /><rect x="9.5" y="4" width="5" height="10" rx="1" /><rect x="16" y="4" width="5" height="13" rx="1" /></svg>
</x-slot:icon>
{{ __('By status') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'active'" @click="filter = 'active'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="2.25" fill="currentColor" stroke="none" /></svg>
</x-slot:icon>
{{ __('Active') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'completed'" @click="filter = 'completed'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" /><path d="m8.5 12.5 2.5 2.5 4.5-5" /></svg>
</x-slot:icon>
{{ __('Completed') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'all'" @click="filter = 'all'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6" /><line x1="4" y1="12" x2="20" y2="12" /><line x1="4" y1="18" x2="20" y2="18" /></svg>
</x-slot:icon>
{{ __('All') }}
</x-ui.segmented-nav.item>
</x-ui.segmented-nav>
</div>
<div class="mt-6" x-show="filter === 'status'" x-cloak>
<x-ui.kanban :columns="$statusColumns" tint="status" assignee-names />
</div>
<div class="mt-6" x-show="filter === 'active'" x-cloak>
<x-ui.kanban :columns="$activeColumns" tint="status" assignee-names />
</div>
<div class="mt-6" x-show="filter === 'completed'" x-cloak>
<x-ui.kanban :columns="$completedColumns" tint="status" assignee-names />
</div>
{{-- min-w-0 + overflow-x-hidden: the horizontal single-lane board's own
list already scrolls itself (kanban's overflow-x-auto on the card
row); this wrapper is a defensive clip so that inner scroll can
never widen the page itself. --}}
<div class="mt-6 min-w-0 overflow-x-hidden" x-show="filter === 'all'" x-cloak>
<x-ui.kanban :columns="$allColumns" orientation="horizontal" assignee-names />
</div>
</x-ui.container>
</x-ui.section>
Installation
php artisan ui:add blocks/projects-board
Registry contract
Install confidence
php artisan ui:add blocks/projects-board
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.0.1
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/projects-board.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
npm: alpinejs
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Projects Board (`projects-board`)
A "Projects" section heading, a client-side view-pill filter row (By status, Active, Completed, All), and a kanban board of project cards grouped by status — each with a folder icon, an assignee, a date range and an amount.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/projects-board
```
## Render it
```blade
<x-blocks.projects-board />
```
## Props
- `title` (string, default `Projects`) — Section heading text.
- `projects` (mixed|null, default `null`) — Array of project cards, each with an id, title, status (backlog|todo|in-progress|done), icon, assignee ({name}), start, end and amount; null falls back to eight sample projects spread across the four statuses.
- `filter` (status|active|completed|all, default `status`) — Which pill is active on load. status shows all four columns; active hides the Completed column; completed shows only Completed; all drops the status grouping and lists every project in one horizontal lane.
## Use when
- Showing a workspace's projects grouped into Not started / Started / In progress / Completed columns, with a quick pill row to switch between By status, Active, Completed and All.
- Wanting each project card to carry an assignee, a start/end date range and a budget amount at a glance.
## Avoid when
- The board needs live drag-and-drop persistence wired to a backend — compose <x-ui.kanban> directly and add name/addable/kanban:change handling.
- There is only one way to view the list; a filter row with nothing to switch is dead weight — drop straight to <x-ui.kanban>.
## Rules
- Render the installed block with `<x-blocks.projects-board />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/projects-board
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/projects-board.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Showing a workspace's projects grouped into Not started / Started / In progress / Completed columns, with a quick pill row to switch between By status, Active, Completed and All.
- Wanting each project card to carry an assignee, a start/end date range and a budget amount at a glance.
Avoid when
- The board needs live drag-and-drop persistence wired to a backend — compose <x-ui.kanban> directly and add name/addable/kanban:change handling.
- There is only one way to view the list; a filter row with nothing to switch is dead weight — drop straight to <x-ui.kanban>.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
Usage
Render the block as a component, passing data where useful:
<x-blocks.projects-board />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
{{--
Props:
- title: section heading text.
- projects: [['id', 'title', 'status' => backlog|todo|in-progress|done,
'icon' => folder|file|doc, 'assignee' => ['name'], 'start', 'end', 'amount']].
Every card is grouped into its status column by `status`; null falls
back to eight sample projects across the four statuses.
- filter: the view pill shown first — status|active|completed|all.
status shows all four columns; active hides Completed; completed shows
only Completed; all drops the status grouping and lists every project
in one horizontal, scrollable lane instead.
Switching pills is client-side (Alpine) — no request round-trip.
--}}
@props([
'title' => 'Projects',
'projects' => null,
'filter' => 'status',
])
@php
$projects ??= [
['id' => 'website-redesign', 'title' => 'Website Redesign', 'status' => 'backlog', 'icon' => 'folder', 'assignee' => ['name' => 'Mara Lindgren'], 'start' => 'Dec 2, 2026', 'end' => 'Dec 12, 2026', 'amount' => '$3,200.00'],
['id' => 'onboarding-flow', 'title' => 'Onboarding Flow', 'status' => 'backlog', 'icon' => 'folder', 'assignee' => ['name' => 'Théo Dubois'], 'start' => 'Jan 5, 2027', 'end' => 'Jan 20, 2027', 'amount' => '$1,800.00'],
['id' => 'mobile-banking-app', 'title' => 'Mobile Banking App', 'status' => 'todo', 'icon' => 'folder', 'assignee' => ['name' => 'Priya Nair'], 'start' => 'Nov 18, 2026', 'end' => 'Dec 2, 2026', 'amount' => '$4,800.00'],
['id' => 'support-portal', 'title' => 'Support Portal', 'status' => 'todo', 'icon' => 'folder', 'assignee' => ['name' => 'Owen Baxter'], 'start' => 'Nov 24, 2026', 'end' => 'Dec 8, 2026', 'amount' => '$2,100.00'],
['id' => 'crm-dashboard', 'title' => 'CRM Dashboard', 'status' => 'in-progress', 'icon' => 'folder', 'assignee' => ['name' => 'Sofia Kowalska'], 'start' => 'Nov 10, 2026', 'end' => 'Nov 24, 2026', 'amount' => '$2,750.00'],
['id' => 'analytics-suite', 'title' => 'Analytics Suite', 'status' => 'in-progress', 'icon' => 'folder', 'assignee' => ['name' => 'Diego Santos'], 'start' => 'Oct 30, 2026', 'end' => 'Dec 1, 2026', 'amount' => '$6,400.00'],
['id' => 'ecommerce-platform', 'title' => 'E-commerce Platform', 'status' => 'done', 'icon' => 'folder', 'assignee' => ['name' => 'Amara Chen'], 'start' => 'Oct 28, 2026', 'end' => 'Nov 15, 2026', 'amount' => '$5,500.00'],
['id' => 'marketing-site', 'title' => 'Marketing Site', 'status' => 'done', 'icon' => 'folder', 'assignee' => ['name' => 'Noah Fischer'], 'start' => 'Oct 1, 2026', 'end' => 'Oct 20, 2026', 'amount' => '$3,000.00'],
];
$filter = in_array($filter, ['status', 'active', 'completed', 'all'], true) ? $filter : 'status';
$statusTitles = [
'backlog' => __('Not started'),
'todo' => __('Started'),
'in-progress' => __('In progress'),
'done' => __('Completed'),
];
$byStatus = collect($projects)->groupBy('status');
$statusColumns = collect($statusTitles)
->map(fn ($label, $status) => [
'id' => $status,
'title' => $label,
'status' => $status,
'cards' => ($byStatus->get($status) ?? collect())->values()->all(),
])
->values()
->all();
$activeColumns = collect($statusColumns)->reject(fn ($column) => $column['status'] === 'done')->values()->all();
$completedColumns = collect($statusColumns)->filter(fn ($column) => $column['status'] === 'done')->values()->all();
// "All" drops the status grouping entirely: every project in one
// horizontal lane (kanban's own responsive scroll, never the page).
$allColumns = [[
'id' => 'all',
'title' => __('All projects'),
'cards' => collect($projects)->values()->all(),
]];
@endphp
<x-ui.section data-slot="projects-board" space="lg" x-data="{ filter: {{ Js::from($filter) }} }" {{ $attributes }}>
<x-ui.container gutter="sm">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-2">
<h2 class="text-xl font-semibold tracking-tight text-foreground sm:text-2xl">{{ __($title) }}</h2>
<x-ui.badge variant="secondary" size="sm" :aria-label="__(':count projects', ['count' => count($projects)])">{{ count($projects) }}</x-ui.badge>
</div>
<x-ui.segmented-nav as="div" size="sm" :label="__('Filter projects')" class="flex-wrap">
<x-ui.segmented-nav.item x-active="filter === 'status'" @click="filter = 'status'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="5" height="16" rx="1" /><rect x="9.5" y="4" width="5" height="10" rx="1" /><rect x="16" y="4" width="5" height="13" rx="1" /></svg>
</x-slot:icon>
{{ __('By status') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'active'" @click="filter = 'active'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="2.25" fill="currentColor" stroke="none" /></svg>
</x-slot:icon>
{{ __('Active') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'completed'" @click="filter = 'completed'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" /><path d="m8.5 12.5 2.5 2.5 4.5-5" /></svg>
</x-slot:icon>
{{ __('Completed') }}
</x-ui.segmented-nav.item>
<x-ui.segmented-nav.item x-active="filter === 'all'" @click="filter = 'all'">
<x-slot:icon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6" /><line x1="4" y1="12" x2="20" y2="12" /><line x1="4" y1="18" x2="20" y2="18" /></svg>
</x-slot:icon>
{{ __('All') }}
</x-ui.segmented-nav.item>
</x-ui.segmented-nav>
</div>
<div class="mt-6" x-show="filter === 'status'" x-cloak>
<x-ui.kanban :columns="$statusColumns" tint="status" assignee-names />
</div>
<div class="mt-6" x-show="filter === 'active'" x-cloak>
<x-ui.kanban :columns="$activeColumns" tint="status" assignee-names />
</div>
<div class="mt-6" x-show="filter === 'completed'" x-cloak>
<x-ui.kanban :columns="$completedColumns" tint="status" assignee-names />
</div>
{{-- min-w-0 + overflow-x-hidden: the horizontal single-lane board's own
list already scrolls itself (kanban's overflow-x-auto on the card
row); this wrapper is a defensive clip so that inner scroll can
never widen the page itself. --}}
<div class="mt-6 min-w-0 overflow-x-hidden" x-show="filter === 'all'" x-cloak>
<x-ui.kanban :columns="$allColumns" orientation="horizontal" assignee-names />
</div>
</x-ui.container>
</x-ui.section>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.1 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0