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:
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />}
|
||||||
<span className="text-xs">{part}</span>
|
<BreadcrumbItem>
|
||||||
</BreadcrumbItem>
|
<span className="text-xs">{part}</span>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user