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