refactor: update UI components and styles for improved consistency and functionality

- Refactored Skeleton component to include data-slot attribute and updated class names.
- Enhanced Switch component with size prop and improved styling.
- Updated Tooltip components to include data-slot attributes and improved styling.
- Refactored global CSS to use custom properties for theming and improved dark mode support.
- Added useIsMobile hook for responsive design handling.
- Updated IPC link implementation for tRPC in Electron.
- Adjusted ProjectsPage layout for better responsiveness.
- Removed outdated Tailwind configuration file and integrated Tailwind CSS with Vite.
This commit is contained in:
Roberto Musso
2026-02-20 00:45:22 +01:00
parent 4180c3d215
commit 99140c2c48
32 changed files with 6390 additions and 1934 deletions

View File

@@ -80,13 +80,54 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
const [editDialog, setEditDialog] = useState<{ id: string; name: string; clientId: string | null } | null>(null);
const [editClientValue, setEditClientValue] = useState<string>(NO_CLIENT_KEY);
// New-project dialog state
const [newProjectOpen, setNewProjectOpen] = useState(false);
const [newProjectName, setNewProjectName] = useState('');
const [newProjectClientId, setNewProjectClientId] = useState<string>(NO_CLIENT_KEY);
const [newProjectSubClientId, setNewProjectSubClientId] = useState<string>(NO_CLIENT_KEY);
// Inline client creation
const [creatingClient, setCreatingClient] = useState(false);
const [newClientName, setNewClientName] = useState('');
// Inline sub-client creation
const [creatingSubClient, setCreatingSubClient] = useState(false);
const [newSubClientName, setNewSubClientName] = useState('');
const { data: projectList = [] } = trpc.projects.list.useQuery(
{ includeArchived: showArchived },
);
const { data: clientList = [] } = trpc.clients.list.useQuery();
// Derived: top-level clients and sub-clients grouped by parentId
const topLevelClients = useMemo(
() => clientList.filter((c) => !c.parentId),
[clientList],
);
const subClientsByParent = useMemo(() => {
const m = new Map<string, typeof clientList>();
for (const c of clientList) {
if (c.parentId) {
const arr = m.get(c.parentId);
if (arr) arr.push(c);
else m.set(c.parentId, [c]);
}
}
return m;
}, [clientList]);
const createClientMutation = trpc.clients.create.useMutation({
onSuccess: () => {
void utils.clients.list.invalidate();
},
});
const createMutation = trpc.projects.create.useMutation({
onSuccess: () => { void utils.projects.list.invalidate(); },
onSuccess: (data, variables) => {
// Auto-expand the matching client group
const groupKey = variables.clientId ?? NO_CLIENT_KEY;
setExpanded((prev) => new Set([...prev, groupKey]));
onSelectProject(data.id);
void utils.projects.list.invalidate();
},
});
const updateMutation = trpc.projects.update.useMutation({
@@ -140,8 +181,58 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
});
}
function handleNewProject() {
createMutation.mutate({ name: 'New Project' });
function handleOpenNewProject() {
setNewProjectName('');
setNewProjectClientId(NO_CLIENT_KEY);
setNewProjectSubClientId(NO_CLIENT_KEY);
setCreatingClient(false);
setNewClientName('');
setCreatingSubClient(false);
setNewSubClientName('');
setNewProjectOpen(true);
}
async function handleCreateProject() {
const name = newProjectName.trim();
if (!name) return;
let resolvedClientId: string | undefined;
// If creating a brand-new client first
if (creatingClient && newClientName.trim()) {
const result = await createClientMutation.mutateAsync({ name: newClientName.trim() });
resolvedClientId = result.id;
// If also creating a sub-client under the new client
if (creatingSubClient && newSubClientName.trim()) {
const subResult = await createClientMutation.mutateAsync({
name: newSubClientName.trim(),
parentId: resolvedClientId,
});
resolvedClientId = subResult.id;
}
} else if (newProjectClientId !== NO_CLIENT_KEY) {
// User picked an existing client
if (creatingSubClient && newSubClientName.trim()) {
// Create a new sub-client under the selected client
const subResult = await createClientMutation.mutateAsync({
name: newSubClientName.trim(),
parentId: newProjectClientId,
});
resolvedClientId = subResult.id;
} else if (newProjectSubClientId !== NO_CLIENT_KEY) {
// User picked an existing sub-client
resolvedClientId = newProjectSubClientId;
} else {
// Just the parent client, no sub-client
resolvedClientId = newProjectClientId;
}
}
createMutation.mutate(
{ name, clientId: resolvedClientId },
{ onSuccess: () => setNewProjectOpen(false) },
);
}
function handleArchiveToggle(id: string, currentStatus: string | null) {
@@ -186,7 +277,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
<Button
variant="outline"
size="icon"
onClick={handleNewProject}
onClick={handleOpenNewProject}
disabled={createMutation.isPending}
aria-label="New Project"
>
@@ -237,7 +328,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
<Button
variant="outline"
size="sm"
onClick={handleNewProject}
onClick={handleOpenNewProject}
disabled={createMutation.isPending}
>
<Plus className="mr-1 h-4 w-4" />
@@ -286,7 +377,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
<div
key={project.id}
className={cn(
'group relative flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
'group flex items-center h-7 rounded-md text-sm cursor-pointer hover:bg-accent transition-colors',
selectedProjectId === project.id && 'bg-sidebar-accent',
project.status === 'archived' && 'opacity-60',
)}
@@ -303,9 +394,8 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'size-5 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
'relative z-10 h-5 w-5 min-h-0 min-w-0 p-0 ml-1 text-muted-foreground hover:bg-muted shrink-0',
'opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100',
)}
tabIndex={-1}
@@ -348,7 +438,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
variant="destructive"
onClick={(e) => {
e.stopPropagation();
setDeleteProjectId({ id: project.id, name: project.name });
@@ -399,6 +489,159 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
</AlertDialogContent>
</AlertDialog>
{/* New Project dialog */}
<Dialog open={newProjectOpen} onOpenChange={setNewProjectOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>New Project</DialogTitle>
<DialogDescription>
Give your project a name and optionally assign it to a client.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
{/* Project name */}
<div className="flex flex-col gap-1.5">
<label htmlFor="new-project-name" className="text-sm font-medium">Project Name</label>
<Input
id="new-project-name"
placeholder="e.g. Website Redesign"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
autoFocus
/>
</div>
{/* Client selection */}
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">Client <span className="text-muted-foreground font-normal">(optional)</span></label>
{creatingClient ? (
<div className="flex items-center gap-2">
<Input
placeholder="New client name"
value={newClientName}
onChange={(e) => setNewClientName(e.target.value)}
className="flex-1"
/>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCreatingClient(false);
setNewClientName('');
setCreatingSubClient(false);
setNewSubClientName('');
}}
>
Cancel
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<Select
value={newProjectClientId}
onValueChange={(v) => {
setNewProjectClientId(v);
setNewProjectSubClientId(NO_CLIENT_KEY);
setCreatingSubClient(false);
setNewSubClientName('');
}}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select a client" />
</SelectTrigger>
<SelectContent>
<SelectItem value={NO_CLIENT_KEY}>None (Internal)</SelectItem>
{topLevelClients.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setCreatingClient(true)}
>
<Plus className="size-3.5 mr-1" />New
</Button>
</div>
)}
</div>
{/* Sub-client selection — only when a client is selected or being created */}
{(newProjectClientId !== NO_CLIENT_KEY || (creatingClient && newClientName.trim())) && (
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">Sub-client <span className="text-muted-foreground font-normal">(optional)</span></label>
{creatingSubClient ? (
<div className="flex items-center gap-2">
<Input
placeholder="New sub-client name"
value={newSubClientName}
onChange={(e) => setNewSubClientName(e.target.value)}
className="flex-1"
/>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCreatingSubClient(false);
setNewSubClientName('');
}}
>
Cancel
</Button>
</div>
) : creatingClient ? (
/* When creating a new client there are no existing sub-clients to pick */
<Button
variant="outline"
size="sm"
className="w-fit"
onClick={() => setCreatingSubClient(true)}
>
<Plus className="size-3.5 mr-1" />New Sub-client
</Button>
) : (
<div className="flex items-center gap-2">
<Select
value={newProjectSubClientId}
onValueChange={setNewProjectSubClientId}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select a sub-client" />
</SelectTrigger>
<SelectContent>
<SelectItem value={NO_CLIENT_KEY}>None</SelectItem>
{(subClientsByParent.get(newProjectClientId) ?? []).map((sc) => (
<SelectItem key={sc.id} value={sc.id}>{sc.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setCreatingSubClient(true)}
>
<Plus className="size-3.5 mr-1" />New
</Button>
</div>
)}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setNewProjectOpen(false)}>Cancel</Button>
<Button
onClick={handleCreateProject}
disabled={!newProjectName.trim() || createMutation.isPending || createClientMutation.isPending}
>
{createMutation.isPending || createClientMutation.isPending ? 'Creating…' : 'Create Project'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit client dialog */}
<Dialog
open={!!editDialog}