2026-02-11 17:55:26 -08:00
|
|
|
'use client';
|
|
|
|
|
|
2026-02-11 18:38:51 -08:00
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
|
2026-02-11 17:55:26 -08:00
|
|
|
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
|
|
|
|
|
|
|
|
|
|
import { StatPill } from '../shared/stat-pill';
|
|
|
|
|
|
|
|
|
|
interface KanbanControlsProps {
|
|
|
|
|
filters: KanbanFilterOptions;
|
|
|
|
|
stats: KanbanStats;
|
|
|
|
|
onFiltersChange: (filters: KanbanFilterOptions) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function KanbanControls({ filters, stats, onFiltersChange }: KanbanControlsProps) {
|
2026-02-11 18:38:51 -08:00
|
|
|
const inputClass =
|
|
|
|
|
'rounded-xl border border-border-soft bg-surface-muted/65 px-3 py-2 text-sm text-text-strong outline-none transition placeholder:text-text-muted focus:border-cyan-300/60 focus:ring-2 focus:ring-cyan-300/25';
|
|
|
|
|
|
2026-02-11 17:55:26 -08:00
|
|
|
return (
|
2026-02-11 18:38:51 -08:00
|
|
|
<section className="grid gap-3">
|
|
|
|
|
<motion.div layout className="flex flex-wrap gap-2.5">
|
2026-02-11 17:55:26 -08:00
|
|
|
<input
|
|
|
|
|
type="search"
|
|
|
|
|
value={filters.query ?? ''}
|
|
|
|
|
onChange={(event) => onFiltersChange({ ...filters, query: event.target.value })}
|
|
|
|
|
placeholder="Search by id/title/labels"
|
2026-02-11 18:38:51 -08:00
|
|
|
className={`${inputClass} min-w-60 flex-1`}
|
2026-02-11 17:55:26 -08:00
|
|
|
/>
|
2026-02-11 18:38:51 -08:00
|
|
|
<select
|
2026-02-11 17:55:26 -08:00
|
|
|
value={filters.type ?? ''}
|
|
|
|
|
onChange={(event) => onFiltersChange({ ...filters, type: event.target.value })}
|
2026-02-11 18:38:51 -08:00
|
|
|
className={`${inputClass} w-40`}
|
|
|
|
|
aria-label="Type filter"
|
|
|
|
|
>
|
|
|
|
|
<option value="">All types</option>
|
|
|
|
|
<option value="task">Task</option>
|
|
|
|
|
<option value="bug">Bug</option>
|
|
|
|
|
<option value="feature">Feature</option>
|
|
|
|
|
<option value="epic">Epic</option>
|
|
|
|
|
<option value="chore">Chore</option>
|
|
|
|
|
</select>
|
|
|
|
|
<select
|
2026-02-11 17:55:26 -08:00
|
|
|
value={filters.priority ?? ''}
|
|
|
|
|
onChange={(event) => onFiltersChange({ ...filters, priority: event.target.value })}
|
2026-02-11 18:38:51 -08:00
|
|
|
className={`${inputClass} w-32`}
|
|
|
|
|
aria-label="Priority filter"
|
|
|
|
|
>
|
|
|
|
|
<option value="">All priorities</option>
|
|
|
|
|
<option value="0">P0</option>
|
|
|
|
|
<option value="1">P1</option>
|
|
|
|
|
<option value="2">P2</option>
|
|
|
|
|
<option value="3">P3</option>
|
|
|
|
|
<option value="4">P4</option>
|
|
|
|
|
</select>
|
|
|
|
|
<label className="inline-flex items-center gap-2 rounded-xl border border-border-soft bg-surface-muted/60 px-3 py-2 text-sm text-text-body">
|
2026-02-11 17:55:26 -08:00
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={filters.showClosed ?? false}
|
|
|
|
|
onChange={(event) => onFiltersChange({ ...filters, showClosed: event.target.checked })}
|
2026-02-11 18:38:51 -08:00
|
|
|
className="h-4 w-4 accent-cyan-400"
|
2026-02-11 17:55:26 -08:00
|
|
|
/>
|
|
|
|
|
Show closed
|
|
|
|
|
</label>
|
2026-02-11 18:38:51 -08:00
|
|
|
</motion.div>
|
|
|
|
|
<motion.div layout className="flex flex-wrap gap-2">
|
2026-02-11 17:55:26 -08:00
|
|
|
<StatPill label="Total" value={stats.total} />
|
|
|
|
|
<StatPill label="Open" value={stats.open} />
|
|
|
|
|
<StatPill label="Active" value={stats.active} />
|
|
|
|
|
<StatPill label="Blocked" value={stats.blocked} />
|
|
|
|
|
<StatPill label="Done" value={stats.done} />
|
2026-02-11 18:38:51 -08:00
|
|
|
<StatPill label="P0" value={stats.p0} tone={stats.p0 > 0 ? 'critical' : 'default'} />
|
|
|
|
|
</motion.div>
|
2026-02-11 17:55:26 -08:00
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|