Polish UI: enhance dialogs, command palette, and page layouts
Expand NewIssueDialog with richer form fields. Add NewProjectDialog. Enhance CommandPalette with more actions and search. Improve CompanySwitcher, EmptyState, and IssueProperties. Flesh out Activity, Companies, Dashboard, and Inbox pages with real content and layouts. Refine sidebar, routing, and dialog context. CSS tweaks for dark theme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
@@ -11,8 +12,21 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { CircleDot, Bot, Hexagon, Target, LayoutDashboard, Inbox } from "lucide-react";
|
||||
import {
|
||||
CircleDot,
|
||||
Bot,
|
||||
Hexagon,
|
||||
Target,
|
||||
LayoutDashboard,
|
||||
Inbox,
|
||||
DollarSign,
|
||||
History,
|
||||
GitBranch,
|
||||
SquarePen,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import type { Issue, Agent, Project } from "@paperclip/shared";
|
||||
|
||||
export function CommandPalette() {
|
||||
@@ -22,6 +36,7 @@ export function CommandPalette() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { openNewIssue } = useDialog();
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
@@ -57,6 +72,11 @@ export function CommandPalette() {
|
||||
navigate(path);
|
||||
}
|
||||
|
||||
const agentName = (id: string | null) => {
|
||||
if (!id) return null;
|
||||
return agents.find((a) => a.id === id)?.name ?? null;
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Search issues, agents, projects..." />
|
||||
@@ -64,7 +84,7 @@ export function CommandPalette() {
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
|
||||
<CommandGroup heading="Pages">
|
||||
<CommandItem onSelect={() => go("/")}>
|
||||
<CommandItem onSelect={() => go("/dashboard")}>
|
||||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</CommandItem>
|
||||
@@ -72,7 +92,7 @@ export function CommandPalette() {
|
||||
<Inbox className="mr-2 h-4 w-4" />
|
||||
Inbox
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/tasks")}>
|
||||
<CommandItem onSelect={() => go("/issues")}>
|
||||
<CircleDot className="mr-2 h-4 w-4" />
|
||||
Issues
|
||||
</CommandItem>
|
||||
@@ -88,42 +108,92 @@ export function CommandPalette() {
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
Agents
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/costs")}>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Costs
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/activity")}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
Activity
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/org")}>
|
||||
<GitBranch className="mr-2 h-4 w-4" />
|
||||
Org Chart
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Actions">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
openNewIssue();
|
||||
}}
|
||||
>
|
||||
<SquarePen className="mr-2 h-4 w-4" />
|
||||
Create new issue
|
||||
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/agents")}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create new agent
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/projects")}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create new project
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
{issues.length > 0 && (
|
||||
<CommandGroup heading="Issues">
|
||||
{issues.slice(0, 10).map((issue) => (
|
||||
<CommandItem key={issue.id} onSelect={() => go(`/issues/${issue.id}`)}>
|
||||
<CircleDot className="mr-2 h-4 w-4" />
|
||||
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
||||
{issue.id.slice(0, 8)}
|
||||
</span>
|
||||
{issue.title}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Issues">
|
||||
{issues.slice(0, 10).map((issue) => (
|
||||
<CommandItem key={issue.id} onSelect={() => go(`/issues/${issue.id}`)}>
|
||||
<CircleDot className="mr-2 h-4 w-4" />
|
||||
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
||||
{issue.id.slice(0, 8)}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{issue.title}</span>
|
||||
{issue.assigneeAgentId && (
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
{agentName(issue.assigneeAgentId)}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{agents.length > 0 && (
|
||||
<CommandGroup heading="Agents">
|
||||
{agents.slice(0, 10).map((agent) => (
|
||||
<CommandItem key={agent.id} onSelect={() => go(`/agents/${agent.id}`)}>
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
{agent.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Agents">
|
||||
{agents.slice(0, 10).map((agent) => (
|
||||
<CommandItem key={agent.id} onSelect={() => go(`/agents/${agent.id}`)}>
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
{agent.name}
|
||||
<span className="text-xs text-muted-foreground ml-2">{agent.role}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{projects.length > 0 && (
|
||||
<CommandGroup heading="Projects">
|
||||
{projects.slice(0, 10).map((project) => (
|
||||
<CommandItem key={project.id} onSelect={() => go(`/projects/${project.id}`)}>
|
||||
<Hexagon className="mr-2 h-4 w-4" />
|
||||
{project.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Projects">
|
||||
{projects.slice(0, 10).map((project) => (
|
||||
<CommandItem key={project.id} onSelect={() => go(`/projects/${project.id}`)}>
|
||||
<Hexagon className="mr-2 h-4 w-4" />
|
||||
{project.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -10,23 +11,39 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
function statusDotColor(status?: string): string {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return "bg-green-400";
|
||||
case "paused":
|
||||
return "bg-yellow-400";
|
||||
case "archived":
|
||||
return "bg-neutral-400";
|
||||
default:
|
||||
return "bg-green-400";
|
||||
}
|
||||
}
|
||||
|
||||
export function CompanySwitcher() {
|
||||
const { companies, selectedCompany, setSelectedCompanyId } = useCompany();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between px-3 py-2 h-auto text-left"
|
||||
className="w-full justify-between px-2 py-1.5 h-auto text-left"
|
||||
>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-xs text-muted-foreground">Company</span>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{selectedCompany && (
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 ${statusDotColor(selectedCompany.status)}`} />
|
||||
)}
|
||||
<span className="text-sm font-medium truncate">
|
||||
{selectedCompany?.name ?? "Select company"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-[220px]">
|
||||
@@ -38,6 +55,7 @@ export function CompanySwitcher() {
|
||||
onClick={() => setSelectedCompanyId(company.id)}
|
||||
className={company.id === selectedCompany?.id ? "bg-accent" : ""}
|
||||
>
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 mr-2 ${statusDotColor(company.status)}`} />
|
||||
<span className="truncate">{company.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -45,11 +63,9 @@ export function CompanySwitcher() {
|
||||
<DropdownMenuItem disabled>No companies</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/companies" className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
Manage Companies
|
||||
</a>
|
||||
<DropdownMenuItem onClick={() => navigate("/companies")}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Manage Companies
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Plus } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -11,10 +12,13 @@ interface EmptyStateProps {
|
||||
export function EmptyState({ icon: Icon, message, action, onAction }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Icon className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<div className="rounded-xl bg-muted/50 p-4 mb-4">
|
||||
<Icon className="h-10 w-10 text-muted-foreground/50" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">{message}</p>
|
||||
{action && onAction && (
|
||||
<Button variant="outline" size="sm" onClick={onAction}>
|
||||
<Button onClick={onAction}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />
|
||||
{action}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Issue } from "@paperclip/shared";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { StatusIcon } from "./StatusIcon";
|
||||
import { PriorityIcon } from "./PriorityIcon";
|
||||
import { formatDate } from "../lib/utils";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface IssuePropertiesProps {
|
||||
@@ -27,6 +30,15 @@ function priorityLabel(priority: string): string {
|
||||
}
|
||||
|
||||
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { data: agents } = useAgents(selectedCompanyId);
|
||||
|
||||
const agentName = (id: string | null) => {
|
||||
if (!id || !agents) return null;
|
||||
const agent = agents.find((a) => a.id === id);
|
||||
return agent?.name ?? id.slice(0, 8);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
@@ -46,30 +58,37 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
||||
<span className="text-sm">{priorityLabel(issue.priority)}</span>
|
||||
</PropertyRow>
|
||||
|
||||
{issue.assigneeAgentId && (
|
||||
<PropertyRow label="Assignee">
|
||||
<span className="text-sm font-mono">{issue.assigneeAgentId.slice(0, 8)}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow label="Assignee">
|
||||
<span className="text-sm">
|
||||
{issue.assigneeAgentId ? agentName(issue.assigneeAgentId) : "Unassigned"}
|
||||
</span>
|
||||
</PropertyRow>
|
||||
|
||||
{issue.projectId && (
|
||||
<PropertyRow label="Project">
|
||||
<span className="text-sm font-mono">{issue.projectId.slice(0, 8)}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow label="Project">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{issue.projectId ? issue.projectId.slice(0, 8) : "None"}
|
||||
</span>
|
||||
</PropertyRow>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-1">
|
||||
<PropertyRow label="ID">
|
||||
<span className="text-sm font-mono">{issue.id.slice(0, 8)}</span>
|
||||
</PropertyRow>
|
||||
{issue.startedAt && (
|
||||
<PropertyRow label="Started">
|
||||
<span className="text-sm">{formatDate(issue.startedAt)}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
{issue.completedAt && (
|
||||
<PropertyRow label="Completed">
|
||||
<span className="text-sm">{formatDate(issue.completedAt)}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
<PropertyRow label="Created">
|
||||
<span className="text-sm">{formatDate(issue.createdAt)}</span>
|
||||
</PropertyRow>
|
||||
<PropertyRow label="Updated">
|
||||
<span className="text-sm">{formatDate(issue.updatedAt)}</span>
|
||||
<span className="text-sm">{timeAgo(issue.updatedAt)}</span>
|
||||
</PropertyRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BreadcrumbBar } from "./BreadcrumbBar";
|
||||
import { PropertiesPanel } from "./PropertiesPanel";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { NewIssueDialog } from "./NewIssueDialog";
|
||||
import { NewProjectDialog } from "./NewProjectDialog";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
||||
@@ -27,18 +28,18 @@ export function Layout() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background text-foreground">
|
||||
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-200 ease-in-out shrink-0 overflow-hidden",
|
||||
"transition-all duration-200 ease-in-out shrink-0 h-full overflow-hidden",
|
||||
sidebarOpen ? "w-60" : "w-0"
|
||||
)}
|
||||
>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 flex flex-col min-w-0 h-full">
|
||||
<BreadcrumbBar />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
@@ -47,6 +48,7 @@ export function Layout() {
|
||||
</div>
|
||||
<CommandPalette />
|
||||
<NewIssueDialog />
|
||||
<NewProjectDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
MoreHorizontal,
|
||||
CircleDot,
|
||||
Minus,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
AlertTriangle,
|
||||
User,
|
||||
Hexagon,
|
||||
Tag,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { Project, Agent } from "@paperclip/shared";
|
||||
|
||||
const statuses = [
|
||||
{ value: "backlog", label: "Backlog", color: "text-muted-foreground" },
|
||||
{ value: "todo", label: "Todo", color: "text-blue-400" },
|
||||
{ value: "in_progress", label: "In Progress", color: "text-yellow-400" },
|
||||
{ value: "in_review", label: "In Review", color: "text-violet-400" },
|
||||
{ value: "done", label: "Done", color: "text-green-400" },
|
||||
];
|
||||
|
||||
const priorities = [
|
||||
{ value: "critical", label: "Critical", icon: AlertTriangle, color: "text-red-400" },
|
||||
{ value: "high", label: "High", icon: ArrowUp, color: "text-orange-400" },
|
||||
{ value: "medium", label: "Medium", icon: Minus, color: "text-yellow-400" },
|
||||
{ value: "low", label: "Low", icon: ArrowDown, color: "text-blue-400" },
|
||||
];
|
||||
|
||||
interface NewIssueDialogProps {
|
||||
onCreated?: () => void;
|
||||
@@ -27,22 +53,50 @@ interface NewIssueDialogProps {
|
||||
|
||||
export function NewIssueDialog({ onCreated }: NewIssueDialogProps) {
|
||||
const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog();
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState(newIssueDefaults.status ?? "todo");
|
||||
const [priority, setPriority] = useState(newIssueDefaults.priority ?? "medium");
|
||||
const [status, setStatus] = useState("todo");
|
||||
const [priority, setPriority] = useState("");
|
||||
const [assigneeId, setAssigneeId] = useState("");
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Popover states
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [priorityOpen, setPriorityOpen] = useState(false);
|
||||
const [assigneeOpen, setAssigneeOpen] = useState(false);
|
||||
const [projectOpen, setProjectOpen] = useState(false);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
|
||||
const { data: agents } = useAgents(selectedCompanyId);
|
||||
|
||||
const projectsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Project[]);
|
||||
return projectsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: projects } = useApi(projectsFetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (newIssueOpen) {
|
||||
setStatus(newIssueDefaults.status ?? "todo");
|
||||
setPriority(newIssueDefaults.priority ?? "");
|
||||
setProjectId(newIssueDefaults.projectId ?? "");
|
||||
}
|
||||
}, [newIssueOpen, newIssueDefaults]);
|
||||
|
||||
function reset() {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setStatus("todo");
|
||||
setPriority("medium");
|
||||
setPriority("");
|
||||
setAssigneeId("");
|
||||
setProjectId("");
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
async function handleSubmit() {
|
||||
if (!selectedCompanyId || !title.trim()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
@@ -51,7 +105,9 @@ export function NewIssueDialog({ onCreated }: NewIssueDialogProps) {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
status,
|
||||
priority,
|
||||
priority: priority || "medium",
|
||||
...(assigneeId ? { assigneeAgentId: assigneeId } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
});
|
||||
reset();
|
||||
closeNewIssue();
|
||||
@@ -61,6 +117,18 @@ export function NewIssueDialog({ onCreated }: NewIssueDialogProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!;
|
||||
const currentPriority = priorities.find((p) => p.value === priority);
|
||||
const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId);
|
||||
const currentProject = (projects ?? []).find((p) => p.id === projectId);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={newIssueOpen}
|
||||
@@ -71,71 +139,232 @@ export function NewIssueDialog({ onCreated }: NewIssueDialogProps) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Issue</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issue-title">Title</Label>
|
||||
<Input
|
||||
id="issue-title"
|
||||
placeholder="Issue title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={cn(
|
||||
"p-0 gap-0",
|
||||
expanded ? "sm:max-w-2xl" : "sm:max-w-lg"
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{selectedCompany && (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded text-xs font-medium">
|
||||
{selectedCompany.name.slice(0, 3).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground/60">›</span>
|
||||
<span>New issue</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issue-desc">Description</Label>
|
||||
<Textarea
|
||||
id="issue-desc"
|
||||
placeholder="Add a description..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="space-y-2 flex-1">
|
||||
<Label>Status</Label>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="backlog">Backlog</SelectItem>
|
||||
<SelectItem value="todo">Todo</SelectItem>
|
||||
<SelectItem value="in_progress">In Progress</SelectItem>
|
||||
<SelectItem value="in_review">In Review</SelectItem>
|
||||
<SelectItem value="done">Done</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<Label>Priority</Label>
|
||||
<Select value={priority} onValueChange={setPriority}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={closeNewIssue}>
|
||||
Cancel
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button type="submit" disabled={!title.trim() || submitting}>
|
||||
{submitting ? "Creating..." : "Create Issue"}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => { reset(); closeNewIssue(); }}
|
||||
>
|
||||
<span className="text-lg leading-none">×</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="px-4 pt-3">
|
||||
<input
|
||||
className="w-full text-base font-medium bg-transparent outline-none placeholder:text-muted-foreground/50"
|
||||
placeholder="Issue title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="px-4 pb-2">
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full bg-transparent outline-none text-sm text-muted-foreground placeholder:text-muted-foreground/40 resize-none",
|
||||
expanded ? "min-h-[200px]" : "min-h-[60px]"
|
||||
)}
|
||||
placeholder="Add description..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Property chips bar */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap">
|
||||
{/* Status chip */}
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<CircleDot className={cn("h-3 w-3", currentStatus.color)} />
|
||||
{currentStatus.label}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-36 p-1" align="start">
|
||||
{statuses.map((s) => (
|
||||
<button
|
||||
key={s.value}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
s.value === status && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
||||
>
|
||||
<CircleDot className={cn("h-3 w-3", s.color)} />
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Priority chip */}
|
||||
<Popover open={priorityOpen} onOpenChange={setPriorityOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
{currentPriority ? (
|
||||
<>
|
||||
<currentPriority.icon className={cn("h-3 w-3", currentPriority.color)} />
|
||||
{currentPriority.label}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Minus className="h-3 w-3 text-muted-foreground" />
|
||||
Priority
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-36 p-1" align="start">
|
||||
{priorities.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
p.value === priority && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setPriority(p.value); setPriorityOpen(false); }}
|
||||
>
|
||||
<p.icon className={cn("h-3 w-3", p.color)} />
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Assignee chip */}
|
||||
<Popover open={assigneeOpen} onOpenChange={setAssigneeOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<User className="h-3 w-3 text-muted-foreground" />
|
||||
{currentAssignee ? currentAssignee.name : "Assignee"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-44 p-1" align="start">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!assigneeId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setAssigneeId(""); setAssigneeOpen(false); }}
|
||||
>
|
||||
No assignee
|
||||
</button>
|
||||
{(agents ?? []).map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
a.id === assigneeId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setAssigneeId(a.id); setAssigneeOpen(false); }}
|
||||
>
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Project chip */}
|
||||
<Popover open={projectOpen} onOpenChange={setProjectOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<Hexagon className="h-3 w-3 text-muted-foreground" />
|
||||
{currentProject ? currentProject.name : "Project"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-44 p-1" align="start">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setProjectId(""); setProjectOpen(false); }}
|
||||
>
|
||||
No project
|
||||
</button>
|
||||
{(projects ?? []).map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
p.id === projectId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setProjectId(p.id); setProjectOpen(false); }}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Labels chip (placeholder) */}
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
||||
<Tag className="h-3 w-3" />
|
||||
Labels
|
||||
</button>
|
||||
|
||||
{/* More (dates) */}
|
||||
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
||||
<MoreHorizontal className="h-3 w-3" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-44 p-1" align="start">
|
||||
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
Start date
|
||||
</button>
|
||||
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
Due date
|
||||
</button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end px-4 py-2.5 border-t border-border">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!title.trim() || submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
251
ui/src/components/NewProjectDialog.tsx
Normal file
251
ui/src/components/NewProjectDialog.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Target,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import type { Goal } from "@paperclip/shared";
|
||||
|
||||
const projectStatuses = [
|
||||
{ value: "backlog", label: "Backlog" },
|
||||
{ value: "planned", label: "Planned" },
|
||||
{ value: "in_progress", label: "In Progress" },
|
||||
{ value: "completed", label: "Completed" },
|
||||
{ value: "cancelled", label: "Cancelled" },
|
||||
];
|
||||
|
||||
interface NewProjectDialogProps {
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
export function NewProjectDialog({ onCreated }: NewProjectDialogProps) {
|
||||
const { newProjectOpen, closeNewProject } = useDialog();
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState("planned");
|
||||
const [goalId, setGoalId] = useState("");
|
||||
const [targetDate, setTargetDate] = useState("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [goalOpen, setGoalOpen] = useState(false);
|
||||
|
||||
const goalsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Goal[]);
|
||||
return goalsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: goals } = useApi(goalsFetcher);
|
||||
|
||||
function reset() {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setStatus("planned");
|
||||
setGoalId("");
|
||||
setTargetDate("");
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!selectedCompanyId || !name.trim()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await projectsApi.create(selectedCompanyId, {
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
status,
|
||||
...(goalId ? { goalId } : {}),
|
||||
...(targetDate ? { targetDate } : {}),
|
||||
});
|
||||
reset();
|
||||
closeNewProject();
|
||||
onCreated?.();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
const currentGoal = (goals ?? []).find((g) => g.id === goalId);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={newProjectOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
reset();
|
||||
closeNewProject();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={cn("p-0 gap-0", expanded ? "sm:max-w-2xl" : "sm:max-w-lg")}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{selectedCompany && (
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded text-xs font-medium">
|
||||
{selectedCompany.name.slice(0, 3).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground/60">›</span>
|
||||
<span>New project</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => { reset(); closeNewProject(); }}
|
||||
>
|
||||
<span className="text-lg leading-none">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="px-4 pt-3">
|
||||
<input
|
||||
className="w-full text-base font-medium bg-transparent outline-none placeholder:text-muted-foreground/50"
|
||||
placeholder="Project name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="px-4 pb-2">
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full bg-transparent outline-none text-sm text-muted-foreground placeholder:text-muted-foreground/40 resize-none",
|
||||
expanded ? "min-h-[160px]" : "min-h-[48px]"
|
||||
)}
|
||||
placeholder="Add description..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Property chips */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap">
|
||||
{/* Status */}
|
||||
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<StatusBadge status={status} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 p-1" align="start">
|
||||
{projectStatuses.map((s) => (
|
||||
<button
|
||||
key={s.value}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
s.value === status && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Goal */}
|
||||
<Popover open={goalOpen} onOpenChange={setGoalOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
||||
<Target className="h-3 w-3 text-muted-foreground" />
|
||||
{currentGoal ? currentGoal.title : "Goal"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-1" align="start">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
||||
!goalId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setGoalId(""); setGoalOpen(false); }}
|
||||
>
|
||||
No goal
|
||||
</button>
|
||||
{(goals ?? []).map((g) => (
|
||||
<button
|
||||
key={g.id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate",
|
||||
g.id === goalId && "bg-accent"
|
||||
)}
|
||||
onClick={() => { setGoalId(g.id); setGoalOpen(false); }}
|
||||
>
|
||||
{g.title}
|
||||
</button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Target date */}
|
||||
<div className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<input
|
||||
type="date"
|
||||
className="bg-transparent outline-none text-xs w-24"
|
||||
value={targetDate}
|
||||
onChange={(e) => setTargetDate(e.target.value)}
|
||||
placeholder="Target date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end px-4 py-2.5 border-t border-border">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!name.trim() || submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? "Creating..." : "Create project"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SquarePen,
|
||||
Building2,
|
||||
ListTodo,
|
||||
LayoutList,
|
||||
} from "lucide-react";
|
||||
import { CompanySwitcher } from "./CompanySwitcher";
|
||||
import { SidebarSection } from "./SidebarSection";
|
||||
@@ -29,16 +30,15 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-60 border-r border-border bg-card flex flex-col shrink-0">
|
||||
<div className="p-3">
|
||||
<CompanySwitcher />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 px-3 pb-2">
|
||||
<aside className="w-60 h-full border-r border-border bg-card flex flex-col">
|
||||
<div className="flex items-center gap-1 p-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<CompanySwitcher />
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={openSearch}
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
@@ -46,7 +46,7 @@ export function Sidebar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground"
|
||||
className="text-muted-foreground shrink-0"
|
||||
onClick={() => openNewIssue()}
|
||||
>
|
||||
<SquarePen className="h-4 w-4" />
|
||||
@@ -63,13 +63,13 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
<SidebarSection label="Work">
|
||||
<SidebarNavItem to="/tasks" label="Issues" icon={CircleDot} />
|
||||
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
|
||||
<SidebarNavItem to="/projects" label="Projects" icon={Hexagon} />
|
||||
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
|
||||
</SidebarSection>
|
||||
|
||||
<SidebarSection label="Company">
|
||||
<SidebarNavItem to="/" label="Dashboard" icon={LayoutDashboard} end />
|
||||
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} />
|
||||
<SidebarNavItem to="/org" label="Org Chart" icon={GitBranch} />
|
||||
<SidebarNavItem to="/agents" label="Agents" icon={Bot} />
|
||||
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
|
||||
|
||||
@@ -37,7 +37,7 @@ function DialogOverlay({
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 duration-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -59,7 +59,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-[0.97] data-[state=open]:zoom-in-[0.97] data-[state=closed]:slide-out-to-top-[1%] data-[state=open]:slide-in-from-top-[1%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-150 ease-[cubic-bezier(0.16,1,0.3,1)] outline-none sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user