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();
}, [isStreaming, streamingEl]);
// Auto-fire daily brief on home page
useEffect(() => {
if (!isHomePage || hasFiredBrief.current || hasTokenQuery.data !== true) return;
@@ -168,7 +167,6 @@ export function AIChatPanel({
}
};
const hasMessages = messages.length > 0 || isStreaming;
// Derived values for home page
@@ -195,12 +193,14 @@ export function AIChatPanel({
<div className="flex-1" />
<button
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"
>
{briefExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
<button
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"
>
<X size={14} />
@@ -210,7 +210,7 @@ export function AIChatPanel({
{!briefExpanded && (
<div className="px-4 pb-3 -mt-1">
<p className="text-xs text-muted-foreground truncate">
{dailyBrief.replace(/[#*_~`>\-]/g, '').slice(0, 120)}...
{dailyBrief.replace(/[#*_~`>-]/g, '').slice(0, 120)}...
</p>
</div>
)}
@@ -479,6 +479,7 @@ function ChatInput({
onChange={(e) => onInputChange(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Ask me anything..."
aria-label="Chat message"
rows={1}
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}
@@ -486,6 +487,7 @@ function ChatInput({
<button
onClick={onSend}
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"
>
<ArrowUp size={16} />

View File

@@ -35,8 +35,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuTrigger,
@@ -163,13 +161,13 @@ function AppShellInner({ children }: AppShellProps) {
<p className="text-xs text-muted-foreground">
Your token is stored securely in the OS keychain.
{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>
</div>
<DialogFooter>
{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} />
Saved
</span>

View File

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

View File

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

View File

@@ -4,21 +4,21 @@ export function PriorityBadge({ priority }: { priority: string | null }) {
switch (priority) {
case 'high':
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" />
High
</span>
);
case 'medium':
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" />
Medium
</span>
);
case 'low':
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" />
Low
</span>

View File

@@ -1,5 +1,7 @@
import { Fragment } from 'react';
import { motion } from 'framer-motion';
import { Calendar, User, Pencil, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import {
@@ -95,9 +97,13 @@ export function TaskRow({
<ContextMenuTrigger asChild>
<Wrapper
{...wrapperProps}
className={`flex flex-col gap-1.5 px-4 py-3 rounded-md border select-none transition-colors ${
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'}`}
className={cn(
'flex flex-col gap-1.5 px-4 py-3 rounded-md border select-none transition-colors',
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)}
>
{/* Row 1: checkbox + title + description */}
@@ -109,7 +115,7 @@ export function TaskRow({
className="mt-0.5 shrink-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}
</div>
{task.description && (
@@ -136,10 +142,12 @@ export function TaskRow({
<Breadcrumb className="shrink-0">
<BreadcrumbList>
{breadcrumb.map((part, i) => (
<BreadcrumbItem key={i}>
<Fragment key={i}>
{i > 0 && <BreadcrumbSeparator />}
<span className="text-xs">{part}</span>
</BreadcrumbItem>
<BreadcrumbItem>
<span className="text-xs">{part}</span>
</BreadcrumbItem>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>

View File

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

View File

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