Skip to content
Brok UI

Loading…

No results

Projects

Open source

A workspace projects panel — a status-grouped kanban board behind a By status / Active / Completed / All filter row.

Preview

resources/views/blocks/projects-board.blade.php Blade
{{--
    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

terminal
php artisan ui:add blocks/projects-board

Installs these dependencies automatically:

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

section container badge segmented-nav kanban

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.

projects-board.md
# 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
root segmented-nav kanban

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.projects-board />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/projects-board.blade.php Blade
{{--
    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