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:
parent
c9e65d435c
commit
aa36671e95
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user