feat(tracker-web): migrate board + item detail to primitives (UX-2.3)

Board: type/priority pills → StatusDot/StatusBadge tones, count pill →
Badge, quick status-move buttons → Button, load errors → AlertBanner
(keeping the existing TooltipProvider + column accents).

Item detail: title/description editor → Input/Textarea/Button, the
status/priority/visibility selects → shared Select, the vote control →
Button, comment composer → Textarea/Button, errors → AlertBanner.
ActionMenu + Timeline (UX-12.2) are untouched.

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:39:15 -07:00
parent c9e65d435c
commit aa36671e95
2 changed files with 76 additions and 81 deletions

View File

@ -4,10 +4,16 @@ import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { PageHeader } from '@bytelyst/dashboard-components'; import { PageHeader } from '@bytelyst/dashboard-components';
import { import {
Button,
Badge,
StatusBadge,
StatusDot,
AlertBanner,
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
type StatusTone,
} from '@/components/ui/Primitives'; } from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context'; import { useAuth } from '@/lib/auth-context';
import { listItems, updateItemStatus, type TrackerItem } from '@/lib/tracker-client'; import { listItems, updateItemStatus, type TrackerItem } from '@/lib/tracker-client';
@ -19,17 +25,17 @@ const COLUMNS: { key: string; label: string; color: string }[] = [
{ key: 'closed', label: 'Closed', color: 'border-t-gray-500' }, { key: 'closed', label: 'Closed', color: 'border-t-gray-500' },
]; ];
const TYPE_DOT: Record<string, string> = { const TYPE_TONE: Record<string, StatusTone> = {
bug: 'bg-red-500', bug: 'danger',
feature: 'bg-blue-500', feature: 'info',
task: 'bg-amber-500', task: 'warning',
}; };
const PRIORITY_LABEL: Record<string, string> = { const PRIORITY_TONE: Record<string, StatusTone> = {
critical: 'text-red-600 dark:text-red-400', critical: 'danger',
high: 'text-orange-600 dark:text-orange-400', high: 'warning',
medium: 'text-yellow-600 dark:text-yellow-400', medium: 'neutral',
low: 'text-green-600 dark:text-green-400', low: 'success',
}; };
export default function BoardPage() { export default function BoardPage() {
@ -71,9 +77,9 @@ export default function BoardPage() {
<p className="-mt-4 text-sm text-muted-foreground">Kanban view of all items</p> <p className="-mt-4 text-sm text-muted-foreground">Kanban view of all items</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>
)} )}
<div className="grid grid-cols-1 gap-4 md:grid-cols-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
@ -86,9 +92,7 @@ export default function BoardPage() {
> >
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold">{col.label}</h3> <h3 className="text-sm font-semibold">{col.label}</h3>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground"> <Badge variant="neutral">{colItems.length}</Badge>
{colItems.length}
</span>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -98,15 +102,14 @@ export default function BoardPage() {
className="rounded-lg border border-border bg-background p-3 shadow-sm transition-shadow hover:shadow-md" className="rounded-lg border border-border bg-background p-3 shadow-sm transition-shadow hover:shadow-md"
> >
<div className="mb-1 flex items-center gap-2"> <div className="mb-1 flex items-center gap-2">
<span <StatusDot tone={TYPE_TONE[item.type] ?? 'neutral'} />
className={`h-2 w-2 rounded-full ${TYPE_DOT[item.type] || 'bg-gray-400'}`}
/>
<span className="text-xs text-muted-foreground">{item.type}</span> <span className="text-xs text-muted-foreground">{item.type}</span>
<span <StatusBadge
className={`ml-auto text-xs font-medium ${PRIORITY_LABEL[item.priority] || ''}`} tone={PRIORITY_TONE[item.priority] ?? 'neutral'}
className="ml-auto"
> >
{item.priority} {item.priority}
</span> </StatusBadge>
</div> </div>
<Tooltip> <Tooltip>
@ -127,16 +130,18 @@ export default function BoardPage() {
</div> </div>
{/* Quick status move */} {/* Quick status move */}
<div className="mt-2 flex gap-1"> <div className="mt-2 flex flex-wrap gap-1">
{COLUMNS.filter(c => c.key !== item.status).map(c => ( {COLUMNS.filter(c => c.key !== item.status).map(c => (
<button <Button
key={c.key} key={c.key}
variant="ghost"
size="sm"
onClick={() => handleStatusChange(item.id, c.key)} onClick={() => handleStatusChange(item.id, c.key)}
className="rounded px-1.5 py-0.5 text-[10px] text-muted-foreground hover:bg-accent hover:text-accent-foreground" className="h-auto px-1.5 py-0.5 text-[10px]"
title={`Move to ${c.label}`} title={`Move to ${c.label}`}
> >
{c.label} {c.label}
</button> </Button>
))} ))}
</div> </div>
</div> </div>

View File

@ -3,7 +3,15 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components'; import { PageHeader, LoadingSpinner } from '@bytelyst/dashboard-components';
import { ActionMenu, Timeline } from '@/components/ui/Primitives'; import {
ActionMenu,
Timeline,
Button,
Input,
Textarea,
Select,
AlertBanner,
} from '@/components/ui/Primitives';
import { useAuth } from '@/lib/auth-context'; import { useAuth } from '@/lib/auth-context';
import { import {
getItem, getItem,
@ -158,40 +166,35 @@ export default function ItemDetailPage() {
/> />
{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>
)} )}
{/* Title/description editor */} {/* Title/description editor */}
<div className="space-y-2"> <div className="space-y-2">
{editing ? ( {editing ? (
<div className="space-y-3"> <div className="space-y-3">
<input <Input
type="text" type="text"
aria-label="Title"
value={editTitle} value={editTitle}
onChange={e => setEditTitle(e.target.value)} onChange={e => setEditTitle(e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-lg font-bold outline-none ring-ring focus:ring-2" className="text-lg font-bold"
/> />
<textarea <Textarea
aria-label="Description"
value={editDescription} value={editDescription}
onChange={e => setEditDescription(e.target.value)} onChange={e => setEditDescription(e.target.value)}
rows={6} rows={6}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<button <Button size="sm" onClick={handleSaveEdit}>
onClick={handleSaveEdit}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Save Save
</button> </Button>
<button <Button size="sm" variant="ghost" onClick={() => setEditing(false)}>
onClick={() => setEditing(false)}
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent"
>
Cancel Cancel
</button> </Button>
</div> </div>
</div> </div>
) : ( ) : (
@ -209,54 +212,45 @@ export default function ItemDetailPage() {
</div> </div>
<div> <div>
<div className="text-xs font-medium text-muted-foreground">Status</div> <div className="text-xs font-medium text-muted-foreground">Status</div>
<select <Select
aria-label="Status"
controlSize="sm"
className="mt-1"
value={item.status} value={item.status}
onChange={e => handleStatusChange(e.target.value)} onChange={e => handleStatusChange(e.target.value)}
className="mt-1 rounded border border-input bg-background px-2 py-1 text-sm" options={STATUSES.map(s => ({ value: s, label: s.replace(/_/g, ' ') }))}
> />
{STATUSES.map(s => (
<option key={s} value={s}>
{s.replace(/_/g, ' ')}
</option>
))}
</select>
</div> </div>
<div> <div>
<div className="text-xs font-medium text-muted-foreground">Priority</div> <div className="text-xs font-medium text-muted-foreground">Priority</div>
<select <Select
aria-label="Priority"
controlSize="sm"
className="mt-1"
value={item.priority} value={item.priority}
onChange={e => handlePriorityChange(e.target.value)} onChange={e => handlePriorityChange(e.target.value)}
className="mt-1 rounded border border-input bg-background px-2 py-1 text-sm" options={PRIORITIES.map(p => ({ value: p, label: p }))}
> />
{PRIORITIES.map(p => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div> </div>
<div> <div>
<div className="text-xs font-medium text-muted-foreground">Visibility</div> <div className="text-xs font-medium text-muted-foreground">Visibility</div>
<select <Select
aria-label="Visibility"
controlSize="sm"
className="mt-1"
value={item.visibility || 'internal'} value={item.visibility || 'internal'}
onChange={e => handleVisibilityChange(e.target.value)} onChange={e => handleVisibilityChange(e.target.value)}
className="mt-1 rounded border border-input bg-background px-2 py-1 text-sm" options={VISIBILITIES.map(v => ({
> value: v,
{VISIBILITIES.map(v => ( label: v === 'public' ? '🌐 Public' : '🔒 Internal',
<option key={v} value={v}> }))}
{v === 'public' ? '🌐 Public' : '🔒 Internal'} />
</option>
))}
</select>
</div> </div>
<div> <div>
<div className="text-xs font-medium text-muted-foreground">Votes</div> <div className="text-xs font-medium text-muted-foreground">Votes</div>
<button <Button variant="outline" size="sm" className="mt-1" onClick={handleVote}>
onClick={handleVote}
className="mt-1 flex items-center gap-1 rounded-md border border-input bg-background px-2 py-1 text-sm hover:bg-accent"
>
{item.voteCount} {item.voteCount}
</button> </Button>
</div> </div>
</div> </div>
@ -291,20 +285,16 @@ export default function ItemDetailPage() {
/> />
<form onSubmit={handleAddComment} className="space-y-2"> <form onSubmit={handleAddComment} className="space-y-2">
<textarea <Textarea
aria-label="Add a comment"
value={newComment} value={newComment}
onChange={e => setNewComment(e.target.value)} onChange={e => setNewComment(e.target.value)}
placeholder="Add a comment..." placeholder="Add a comment..."
rows={3} rows={3}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none ring-ring focus:ring-2"
/> />
<button <Button type="submit" disabled={!newComment.trim()}>
type="submit"
disabled={!newComment.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
Comment Comment
</button> </Button>
</form> </form>
</div> </div>
</div> </div>