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:
Forgotten
2026-02-17 10:53:20 -06:00
parent 102f61c96d
commit d912670f72
22 changed files with 1301 additions and 254 deletions

View 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">&rsaquo;</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">&times;</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>
);
}