feat(tracker-web): migrate items page controls + create modal to primitives (UX-2.1)

Replace the raw search input, the three filter selects, the New Item
button, and the hand-rolled create modal with @bytelyst/ui Input/Select/
Field/Button/Modal (via the Primitives adapter). The shared Modal closes
the focus-trap/Esc/scroll-lock a11y gap. Swap the inline type/status/
priority cell pills to StatusBadge with token-driven tones.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-29 06:33:20 -07:00
parent 64e6cc11a1
commit a7a6f191ca

View File

@ -4,28 +4,40 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
import Link from 'next/link';
import { DataTable, type ColumnDef } from '@bytelyst/data-table';
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
import {
Button,
Input,
Select,
Textarea,
Field,
FieldLabel,
Modal,
StatusBadge,
AlertBanner,
type StatusTone,
} from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context';
import { listItems, createItem, deleteItem, type TrackerItem } from '@/lib/tracker-client';
const TYPE_BADGE: Record<string, string> = {
bug: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
feature: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
task: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
const TYPE_TONE: Record<string, StatusTone> = {
bug: 'danger',
feature: 'info',
task: 'warning',
};
const PRIORITY_BADGE: Record<string, string> = {
critical: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
low: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
const PRIORITY_TONE: Record<string, StatusTone> = {
critical: 'danger',
high: 'warning',
medium: 'neutral',
low: 'success',
};
const STATUS_BADGE: Record<string, string> = {
open: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
in_progress: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
done: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300',
closed: 'bg-gray-100 text-gray-800 dark:bg-gray-800/30 dark:text-gray-300',
wont_fix: 'bg-gray-100 text-gray-600 dark:bg-gray-800/30 dark:text-gray-400',
const STATUS_TONE: Record<string, StatusTone> = {
open: 'success',
in_progress: 'warning',
done: 'success',
closed: 'neutral',
wont_fix: 'neutral',
};
export default function ItemsListPage() {
@ -139,11 +151,9 @@ export default function ItemsListPage() {
accessorKey: 'type',
header: 'Type',
cell: ({ row }) => (
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${TYPE_BADGE[row.original.type] || ''}`}
>
<StatusBadge tone={TYPE_TONE[row.original.type] ?? 'neutral'}>
{row.original.type}
</span>
</StatusBadge>
),
},
{
@ -151,11 +161,9 @@ export default function ItemsListPage() {
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => (
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_BADGE[row.original.status] || ''}`}
>
<StatusBadge tone={STATUS_TONE[row.original.status] ?? 'neutral'} dot>
{row.original.status.replace(/_/g, ' ')}
</span>
</StatusBadge>
),
},
{
@ -163,11 +171,9 @@ export default function ItemsListPage() {
accessorKey: 'priority',
header: 'Priority',
cell: ({ row }) => (
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${PRIORITY_BADGE[row.original.priority] || ''}`}
>
<StatusBadge tone={PRIORITY_TONE[row.original.priority] ?? 'neutral'}>
{row.original.priority}
</span>
</StatusBadge>
),
},
{ id: 'voteCount', accessorKey: 'voteCount', header: 'Votes' },
@ -177,12 +183,14 @@ export default function ItemsListPage() {
header: 'Actions',
enableSorting: false,
cell: ({ row }) => (
<button
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(row.original.id)}
className="text-xs text-muted-foreground hover:text-destructive"
className="text-muted-foreground hover:text-destructive"
>
Delete
</button>
</Button>
),
},
],
@ -195,65 +203,61 @@ export default function ItemsListPage() {
<PageHeader
title="Items"
breadcrumbs={[{ label: 'Dashboard', href: '/dashboard' }, { label: 'Items' }]}
actions={
<button
onClick={() => setShowCreate(true)}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
+ New Item
</button>
}
actions={<Button onClick={() => setShowCreate(true)}>+ New Item</Button>}
/>
<p className="-mt-4 text-sm text-muted-foreground">{total} items total</p>
{error && (
<div className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
<AlertBanner tone="error" title="Something went wrong">
{error}
</div>
</AlertBanner>
)}
{/* Filters */}
<div className="flex flex-wrap gap-3">
<input
<Input
type="text"
placeholder="Search..."
aria-label="Search items"
value={search}
onChange={e => setSearch(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-1.5 text-sm outline-none ring-ring focus:ring-2"
/>
<select
<Select
aria-label="Filter by type"
value={typeFilter}
onChange={e => setTypeFilter(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-1.5 text-sm"
>
<option value="">All types</option>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
<option value="task">Task</option>
</select>
<select
options={[
{ value: '', label: 'All types' },
{ value: 'bug', label: 'Bug' },
{ value: 'feature', label: 'Feature' },
{ value: 'task', label: 'Task' },
]}
/>
<Select
aria-label="Filter by status"
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-1.5 text-sm"
>
<option value="">All statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="done">Done</option>
<option value="closed">Closed</option>
<option value="wont_fix">Won&apos;t Fix</option>
</select>
<select
options={[
{ value: '', label: 'All statuses' },
{ value: 'open', label: 'Open' },
{ value: 'in_progress', label: 'In Progress' },
{ value: 'done', label: 'Done' },
{ value: 'closed', label: 'Closed' },
{ value: 'wont_fix', label: "Won't Fix" },
]}
/>
<Select
aria-label="Filter by priority"
value={priorityFilter}
onChange={e => setPriorityFilter(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-1.5 text-sm"
>
<option value="">All priorities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
options={[
{ value: '', label: 'All priorities' },
{ value: 'critical', label: 'Critical' },
{ value: 'high', label: 'High' },
{ value: 'medium', label: 'Medium' },
{ value: 'low', label: 'Low' },
]}
/>
</div>
{/* Items table — @bytelyst/data-table (Wave 9.C.9) */}
@ -276,78 +280,57 @@ export default function ItemsListPage() {
)}
{/* Create modal */}
{showCreate && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-xl">
<h2 className="mb-4 text-lg font-bold">New Item</h2>
<form onSubmit={handleCreate} className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Title</label>
<input
type="text"
required
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Type</label>
<select
value={newType}
onChange={e => setNewType(e.target.value as 'bug' | 'feature' | 'task')}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
<option value="task">Task</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Priority</label>
<select
value={newPriority}
onChange={e =>
setNewPriority(e.target.value as 'critical' | 'high' | 'medium' | 'low')
}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Description</label>
<textarea
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
rows={4}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
/>
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setShowCreate(false)}
className="rounded-md px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent"
>
Cancel
</button>
<button
type="submit"
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Create
</button>
</div>
</form>
<Modal open={showCreate} onOpenChange={setShowCreate} title="New Item">
<form onSubmit={handleCreate} className="space-y-4">
<Input
label="Title"
type="text"
required
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
/>
<div className="grid grid-cols-2 gap-4">
<Select
label="Type"
value={newType}
onChange={e => setNewType(e.target.value as 'bug' | 'feature' | 'task')}
options={[
{ value: 'bug', label: 'Bug' },
{ value: 'feature', label: 'Feature' },
{ value: 'task', label: 'Task' },
]}
/>
<Select
label="Priority"
value={newPriority}
onChange={e =>
setNewPriority(e.target.value as 'critical' | 'high' | 'medium' | 'low')
}
options={[
{ value: 'critical', label: 'Critical' },
{ value: 'high', label: 'High' },
{ value: 'medium', label: 'Medium' },
{ value: 'low', label: 'Low' },
]}
/>
</div>
</div>
)}
<Field>
<FieldLabel htmlFor="new-item-description">Description</FieldLabel>
<Textarea
id="new-item-description"
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
rows={4}
/>
</Field>
<div className="flex justify-end gap-3">
<Button type="button" variant="ghost" onClick={() => setShowCreate(false)}>
Cancel
</Button>
<Button type="submit">Create</Button>
</div>
</form>
</Modal>
</div>
);
}