feat(AIChatPanel): add aria-labels for accessibility; clean up unused lines

feat(AppShell): improve token storage message styling for better visibility
feat(ProjectDetail): implement skeleton loading state for project details
fix(ProjectSidebar): refactor variable declaration for clarity
style(PriorityBadge): enhance priority badge colors for better contrast
refactor(TaskRow): simplify className handling with utility function
fix(TasksPage): replace loader icon with clock icon for in-progress tasks
feat(TimelinePage): enhance empty state with descriptive messaging and icon
This commit is contained in:
Roberto Musso
2026-03-01 10:40:22 +01:00
parent d3e82a3ebb
commit e005872ba0
8 changed files with 53 additions and 26 deletions

View File

@@ -121,7 +121,6 @@ export function AIChatPanel({
return () => observer.disconnect(); return () => observer.disconnect();
}, [isStreaming, streamingEl]); }, [isStreaming, streamingEl]);
// Auto-fire daily brief on home page // Auto-fire daily brief on home page
useEffect(() => { useEffect(() => {
if (!isHomePage || hasFiredBrief.current || hasTokenQuery.data !== true) return; if (!isHomePage || hasFiredBrief.current || hasTokenQuery.data !== true) return;
@@ -168,7 +167,6 @@ export function AIChatPanel({
} }
}; };
const hasMessages = messages.length > 0 || isStreaming; const hasMessages = messages.length > 0 || isStreaming;
// Derived values for home page // Derived values for home page
@@ -195,12 +193,14 @@ export function AIChatPanel({
<div className="flex-1" /> <div className="flex-1" />
<button <button
onClick={() => setBriefExpanded((v) => !v)} onClick={() => setBriefExpanded((v) => !v)}
aria-label={briefExpanded ? 'Collapse brief' : 'Expand brief'}
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60" className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
> >
{briefExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />} {briefExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button> </button>
<button <button
onClick={() => setBriefDismissed(true)} onClick={() => setBriefDismissed(true)}
aria-label="Dismiss brief"
className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60" className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
> >
<X size={14} /> <X size={14} />
@@ -210,7 +210,7 @@ export function AIChatPanel({
{!briefExpanded && ( {!briefExpanded && (
<div className="px-4 pb-3 -mt-1"> <div className="px-4 pb-3 -mt-1">
<p className="text-xs text-muted-foreground truncate"> <p className="text-xs text-muted-foreground truncate">
{dailyBrief.replace(/[#*_~`>\-]/g, '').slice(0, 120)}... {dailyBrief.replace(/[#*_~`>-]/g, '').slice(0, 120)}...
</p> </p>
</div> </div>
)} )}
@@ -479,6 +479,7 @@ function ChatInput({
onChange={(e) => onInputChange(e.target.value)} onChange={(e) => onInputChange(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
placeholder="Ask me anything..." placeholder="Ask me anything..."
aria-label="Chat message"
rows={1} rows={1}
className="flex-1 resize-none bg-transparent text-sm placeholder:text-muted-foreground outline-none max-h-[7.5rem] overflow-y-auto" className="flex-1 resize-none bg-transparent text-sm placeholder:text-muted-foreground outline-none max-h-[7.5rem] overflow-y-auto"
style={{ fieldSizing: 'content' } as React.CSSProperties} style={{ fieldSizing: 'content' } as React.CSSProperties}
@@ -486,6 +487,7 @@ function ChatInput({
<button <button
onClick={onSend} onClick={onSend}
disabled={!input.trim() || isStreaming} disabled={!input.trim() || isStreaming}
aria-label="Send message"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-all hover:bg-primary/90 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100" className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-all hover:bg-primary/90 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100"
> >
<ArrowUp size={16} /> <ArrowUp size={16} />

View File

@@ -35,8 +35,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -163,13 +161,13 @@ function AppShellInner({ children }: AppShellProps) {
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Your token is stored securely in the OS keychain. Your token is stored securely in the OS keychain.
{hasTokenQuery.data === true && ( {hasTokenQuery.data === true && (
<span className="text-green-600 ml-1">A token is currently stored.</span> <span className="text-green-600 dark:text-green-400 ml-1">A token is currently stored.</span>
)} )}
</p> </p>
</div> </div>
<DialogFooter> <DialogFooter>
{saved && ( {saved && (
<span className="flex items-center gap-1 text-sm text-green-600 mr-auto"> <span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400 mr-auto">
<Check size={14} /> <Check size={14} />
Saved Saved
</span> </span>

View File

@@ -4,6 +4,7 @@ import { format } from 'date-fns';
import { useNavigate } from '@tanstack/react-router'; import { useNavigate } from '@tanstack/react-router';
import { trpc } from '@/lib/trpc'; import { trpc } from '@/lib/trpc';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription } from '@/components/ui/item'; import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription } from '@/components/ui/item';
import { import {
Breadcrumb, Breadcrumb,
@@ -167,8 +168,17 @@ export function ProjectDetail({ projectId }: ProjectDetailProps) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground"> <div className="p-6 flex flex-col gap-6">
Loading project... <div className="flex flex-col gap-1">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-56" />
</div>
<div className="grid grid-cols-3 gap-4">
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
<Skeleton className="h-20 rounded-lg" />
</div>
<Skeleton className="h-16 rounded-lg" />
</div> </div>
); );
} }

View File

@@ -322,7 +322,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
if (editCreatingClient && editNewClientName.trim()) { if (editCreatingClient && editNewClientName.trim()) {
// Create a new client // Create a new client
const result = await createClientMutation.mutateAsync({ name: editNewClientName.trim() }); const result = await createClientMutation.mutateAsync({ name: editNewClientName.trim() });
let parentId = result.id; const parentId = result.id;
if (editCreatingSubClient && editNewSubClientName.trim()) { if (editCreatingSubClient && editNewSubClientName.trim()) {
// Also create a sub-client under the new client // Also create a sub-client under the new client

View File

@@ -4,21 +4,21 @@ export function PriorityBadge({ priority }: { priority: string | null }) {
switch (priority) { switch (priority) {
case 'high': case 'high':
return ( return (
<span className="inline-flex items-center gap-1 text-xs"> <span className="inline-flex items-center gap-1 text-xs text-red-600 dark:text-red-400">
<ArrowUp className="h-3 w-3" /> <ArrowUp className="h-3 w-3" />
High High
</span> </span>
); );
case 'medium': case 'medium':
return ( return (
<span className="inline-flex items-center gap-1 text-xs"> <span className="inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
<ArrowRight className="h-3 w-3" /> <ArrowRight className="h-3 w-3" />
Medium Medium
</span> </span>
); );
case 'low': case 'low':
return ( return (
<span className="inline-flex items-center gap-1 text-xs"> <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<ArrowDown className="h-3 w-3" /> <ArrowDown className="h-3 w-3" />
Low Low
</span> </span>

View File

@@ -1,5 +1,7 @@
import { Fragment } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Calendar, User, Pencil, Trash2 } from 'lucide-react'; import { Calendar, User, Pencil, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
@@ -95,9 +97,13 @@ export function TaskRow({
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<Wrapper <Wrapper
{...wrapperProps} {...wrapperProps}
className={`flex flex-col gap-1.5 px-4 py-3 rounded-md border select-none transition-colors ${ className={cn(
isDone ? 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-900' : 'bg-card border-border' 'flex flex-col gap-1.5 px-4 py-3 rounded-md border select-none transition-colors',
} ${onClick ? 'cursor-pointer hover:bg-accent/50' : 'cursor-default'}`} isDone
? 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-900'
: 'bg-card border-border',
onClick ? 'cursor-pointer hover:bg-accent/50' : 'cursor-default',
)}
onClick={() => onClick?.(task)} onClick={() => onClick?.(task)}
> >
{/* Row 1: checkbox + title + description */} {/* Row 1: checkbox + title + description */}
@@ -109,7 +115,7 @@ export function TaskRow({
className="mt-0.5 shrink-0" className="mt-0.5 shrink-0"
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className={`text-sm font-semibold ${isDone ? 'line-through text-muted-foreground' : ''}`}> <div className={cn('text-sm font-medium', isDone && 'line-through text-muted-foreground')}>
{task.title} {task.title}
</div> </div>
{task.description && ( {task.description && (
@@ -136,10 +142,12 @@ export function TaskRow({
<Breadcrumb className="shrink-0"> <Breadcrumb className="shrink-0">
<BreadcrumbList> <BreadcrumbList>
{breadcrumb.map((part, i) => ( {breadcrumb.map((part, i) => (
<BreadcrumbItem key={i}> <Fragment key={i}>
{i > 0 && <BreadcrumbSeparator />} {i > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem>
<span className="text-xs">{part}</span> <span className="text-xs">{part}</span>
</BreadcrumbItem> </BreadcrumbItem>
</Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>

View File

@@ -4,7 +4,7 @@ import { useFloatingChat } from '@/context/FloatingChatContext';
import { import {
ClipboardCheck, ClipboardCheck,
ListTodo, ListTodo,
Loader2, Clock,
CheckCircle2, CheckCircle2,
Plus, Plus,
Search, Search,
@@ -147,7 +147,7 @@ function TasksPage() {
</Item> </Item>
<Item variant="muted" className="bg-sky-50 dark:bg-sky-950/30"> <Item variant="muted" className="bg-sky-50 dark:bg-sky-950/30">
<ItemMedia variant="icon"> <ItemMedia variant="icon">
<Loader2 /> <Clock />
</ItemMedia> </ItemMedia>
<ItemContent> <ItemContent>
<ItemTitle>{stats.inProgress}</ItemTitle> <ItemTitle>{stats.inProgress}</ItemTitle>

View File

@@ -1,12 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { useEffect, useRef, useState, useMemo } from 'react'; import { useEffect, useRef, useState, useMemo } from 'react';
import { Plus } from 'lucide-react'; import { Plus, ChartGantt } from 'lucide-react';
import { useFloatingChat } from '@/context/FloatingChatContext'; import { useFloatingChat } from '@/context/FloatingChatContext';
import { trpc } from '@/lib/trpc'; import { trpc } from '@/lib/trpc';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { GanttChart, type GanttCheckpoint } from '@/components/timeline/GanttChart'; import { GanttChart, type GanttCheckpoint } from '@/components/timeline/GanttChart';
import { AddCheckpointDialog } from '@/components/timeline/AddCheckpointDialog'; import { AddCheckpointDialog } from '@/components/timeline/AddCheckpointDialog';
import { EditCheckpointDialog } from '@/components/timeline/EditCheckpointDialog'; import { EditCheckpointDialog } from '@/components/timeline/EditCheckpointDialog';
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty';
export const Route = createFileRoute('/timeline')({ export const Route = createFileRoute('/timeline')({
component: TimelinePage, component: TimelinePage,
@@ -107,9 +108,17 @@ function TimelinePage() {
{/* Gantt Chart */} {/* Gantt Chart */}
{ganttCheckpoints.length === 0 ? ( {ganttCheckpoints.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-12 border rounded-md bg-muted/30"> <Empty>
No checkpoints yet. Click "+ Add" to create your first milestone. <EmptyHeader>
</div> <EmptyMedia variant="icon">
<ChartGantt />
</EmptyMedia>
<EmptyTitle>No milestones yet</EmptyTitle>
<EmptyDescription>
Click "+ Add" to create your first project checkpoint.
</EmptyDescription>
</EmptyHeader>
</Empty>
) : ( ) : (
<div className="border rounded-md p-4 bg-card"> <div className="border rounded-md p-4 bg-card">
<GanttChart <GanttChart