feat(tracker-web): SegmentedControl view toggle + board Tooltips (UX-12.1)

Replace the bespoke roadmap board/list toggle buttons (hardcoded blue-600) with
the shared SegmentedControl, and wrap truncated board-card titles in Tooltip.
Update the e2e toggle selector from button to radio for SegmentedControl.

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-28 21:01:58 -07:00
parent 328e307212
commit ddf25cf501
4 changed files with 104 additions and 91 deletions

View File

@ -215,8 +215,11 @@ pnpm build # final gate
## UX-12 — Detail & board richness (Tabs · Tooltip · Drawer · Timeline · rich-text) ## UX-12 — Detail & board richness (Tabs · Tooltip · Drawer · Timeline · rich-text)
- [ ] **12.1** `/dashboard` board↔list: use `SegmentedControl` (or `Tabs`) for the view toggle - [x] **12.1** `/dashboard` board↔list: use `SegmentedControl` (or `Tabs`) for the view toggle
instead of bespoke buttons; `Tooltip` on truncated titles / status dots. instead of bespoke buttons; `Tooltip` on truncated titles / status dots.
(SegmentedControl now drives the roadmap board/list toggle — the app's only board↔list
toggle — replacing the bespoke `blue-600` buttons; `Tooltip` wraps the truncated board
card titles. e2e toggle selector updated button→radio. UX-12.1 verified: tc/lint/test 162 ✓/build/e2e 18 ✓)
- [ ] **12.2** Item detail: move row/item actions into an `ActionMenu`, and render the item's - [ ] **12.2** Item detail: move row/item actions into an `ActionMenu`, and render the item's
activity/comment history with `Timeline`. activity/comment history with `Timeline`.
- [ ] **12.3** _(stretch — needs HTML-capable description/comment storage)_ Swap the plain - [ ] **12.3** _(stretch — needs HTML-capable description/comment storage)_ Swap the plain

View File

@ -309,7 +309,8 @@ test.describe('Tracker — Public Roadmap', () => {
test('can toggle between board and list view', async ({ page }) => { test('can toggle between board and list view', async ({ page }) => {
await page.goto('/roadmap'); await page.goto('/roadmap');
await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
await page.getByRole('button', { name: 'List', exact: true }).click(); // The view toggle is a shared SegmentedControl (role=radio), UX-12.1.
await page.getByRole('radio', { name: 'List', exact: true }).click();
// List view still shows the items (rendered as rows, not a <table>) // List view still shows the items (rendered as rows, not a <table>)
await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Dark mode toggle' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Export to CSV' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Export to CSV' })).toBeVisible();

View File

@ -3,6 +3,12 @@
import { useEffect, useState, useCallback } from 'react'; 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 {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} 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';
@ -56,88 +62,95 @@ export default function BoardPage() {
}; };
return ( return (
<div className="space-y-6"> <TooltipProvider>
<PageHeader <div className="space-y-6">
title="Board" <PageHeader
breadcrumbs={[{ label: 'Dashboard', href: '/dashboard' }, { label: 'Board' }]} title="Board"
/> breadcrumbs={[{ label: 'Dashboard', href: '/dashboard' }, { label: 'Board' }]}
<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"> <div className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
{error} {error}
</div> </div>
)} )}
<div className="grid grid-cols-1 gap-4 md:grid-cols-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{COLUMNS.map(col => { {COLUMNS.map(col => {
const colItems = items.filter(i => i.status === col.key); const colItems = items.filter(i => i.status === col.key);
return ( return (
<div <div
key={col.key} key={col.key}
className={`rounded-xl border border-border border-t-4 ${col.color} bg-card p-3`} className={`rounded-xl border border-border border-t-4 ${col.color} bg-card p-3`}
> >
<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"> <span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
{colItems.length} {colItems.length}
</span> </span>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{colItems.map(item => ( {colItems.map(item => (
<div <div
key={item.id} key={item.id}
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">
<span
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={`ml-auto text-xs font-medium ${PRIORITY_LABEL[item.priority] || ''}`}
>
{item.priority}
</span>
</div>
<Link
href={`/dashboard/items/${item.id}`}
className="text-sm font-medium leading-tight hover:text-primary hover:underline"
> >
{item.title} <div className="mb-1 flex items-center gap-2">
</Link> <span
className={`h-2 w-2 rounded-full ${TYPE_DOT[item.type] || 'bg-gray-400'}`}
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground"> />
{item.voteCount > 0 && <span>{item.voteCount} votes</span>} <span className="text-xs text-muted-foreground">{item.type}</span>
{item.commentCount > 0 && <span>{item.commentCount} comments</span>} <span
</div> className={`ml-auto text-xs font-medium ${PRIORITY_LABEL[item.priority] || ''}`}
{/* Quick status move */}
<div className="mt-2 flex gap-1">
{COLUMNS.filter(c => c.key !== item.status).map(c => (
<button
key={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"
title={`Move to ${c.label}`}
> >
{c.label} {item.priority}
</button> </span>
))} </div>
</div>
</div>
))}
{colItems.length === 0 && ( <Tooltip>
<div className="py-8 text-center text-xs text-muted-foreground">No items</div> <TooltipTrigger asChild>
)} <Link
href={`/dashboard/items/${item.id}`}
className="block truncate text-sm font-medium leading-tight hover:text-primary hover:underline"
>
{item.title}
</Link>
</TooltipTrigger>
<TooltipContent>{item.title}</TooltipContent>
</Tooltip>
<div className="mt-2 flex items-center gap-3 text-xs text-muted-foreground">
{item.voteCount > 0 && <span>{item.voteCount} votes</span>}
{item.commentCount > 0 && <span>{item.commentCount} comments</span>}
</div>
{/* Quick status move */}
<div className="mt-2 flex gap-1">
{COLUMNS.filter(c => c.key !== item.status).map(c => (
<button
key={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"
title={`Move to ${c.label}`}
>
{c.label}
</button>
))}
</div>
</div>
))}
{colItems.length === 0 && (
<div className="py-8 text-center text-xs text-muted-foreground">No items</div>
)}
</div>
</div> </div>
</div> );
); })}
})} </div>
</div> </div>
</div> </TooltipProvider>
); );
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { SegmentedControl } from '@/components/ui/Primitives';
import { import {
getRoadmapItems, getRoadmapItems,
getRoadmapStats, getRoadmapStats,
@ -264,20 +265,15 @@ export default function RoadmapPage() {
<option value="bug">Bugs</option> <option value="bug">Bugs</option>
<option value="task">Tasks</option> <option value="task">Tasks</option>
</select> </select>
<div className="flex border border-slate-300 dark:border-slate-600 rounded-lg overflow-hidden"> <SegmentedControl
<button aria-label="Roadmap view"
onClick={() => setView('board')} value={view}
className={`px-3 py-2 text-sm ${view === 'board' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300'}`} onValueChange={v => setView(v as 'board' | 'list')}
> options={[
Board { value: 'board', label: 'Board' },
</button> { value: 'list', label: 'List' },
<button ]}
onClick={() => setView('list')} />
className={`px-3 py-2 text-sm ${view === 'list' ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300'}`}
>
List
</button>
</div>
</div> </div>
{loading ? ( {loading ? (