feat: add new layout system (Slack/ClickUp inspired)

This commit is contained in:
CREDO23 2026-01-08 19:10:40 +02:00
parent 2fd38615e8
commit a919f8d9ee
28 changed files with 3059 additions and 0 deletions

View file

@ -0,0 +1,60 @@
"use client";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { Workspace } from "../../types/layout.types";
import { WorkspaceAvatar } from "./WorkspaceAvatar";
interface IconRailProps {
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onAddWorkspace: () => void;
className?: string;
}
export function IconRail({
workspaces,
activeWorkspaceId,
onWorkspaceSelect,
onAddWorkspace,
className,
}: IconRailProps) {
return (
<div className={cn("flex h-full w-14 flex-col items-center", className)}>
<ScrollArea className="w-full">
<div className="flex flex-col items-center gap-2 px-1.5 py-3">
{workspaces.map((workspace) => (
<WorkspaceAvatar
key={workspace.id}
name={workspace.name}
isActive={workspace.id === activeWorkspaceId}
onClick={() => onWorkspaceSelect(workspace.id)}
size="md"
/>
))}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onAddWorkspace}
className="h-10 w-10 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
>
<Plus className="h-5 w-5 text-muted-foreground" />
<span className="sr-only">Add workspace</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
Add workspace
</TooltipContent>
</Tooltip>
</div>
</ScrollArea>
</div>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import type { LucideIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
interface NavIconProps {
icon: LucideIcon;
label: string;
isActive?: boolean;
onClick?: () => void;
}
export function NavIcon({ icon: Icon, label, isActive, onClick }: NavIconProps) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onClick}
className={cn("h-10 w-10 rounded-lg", isActive && "bg-accent text-accent-foreground")}
>
<Icon className="h-5 w-5" />
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{label}
</TooltipContent>
</Tooltip>
);
}

View file

@ -0,0 +1,72 @@
"use client";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
interface WorkspaceAvatarProps {
name: string;
isActive?: boolean;
onClick?: () => void;
size?: "sm" | "md";
}
/**
* Generates a consistent color based on workspace name
*/
function stringToColor(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const colors = [
"#6366f1", // indigo
"#22c55e", // green
"#f59e0b", // amber
"#ef4444", // red
"#8b5cf6", // violet
"#06b6d4", // cyan
"#ec4899", // pink
"#14b8a6", // teal
];
return colors[Math.abs(hash) % colors.length];
}
/**
* Gets initials from workspace name (max 2 chars)
*/
function getInitials(name: string): string {
const words = name.trim().split(/\s+/);
if (words.length >= 2) {
return (words[0][0] + words[1][0]).toUpperCase();
}
return name.slice(0, 2).toUpperCase();
}
export function WorkspaceAvatar({ name, isActive, onClick, size = "md" }: WorkspaceAvatarProps) {
const bgColor = stringToColor(name);
const initials = getInitials(name);
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onClick}
className={cn(
"flex items-center justify-center rounded-lg font-semibold text-white transition-all",
"hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
sizeClasses,
isActive && "ring-2 ring-primary ring-offset-1 ring-offset-background"
)}
style={{ backgroundColor: bgColor }}
>
{initials}
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{name}
</TooltipContent>
</Tooltip>
);
}

View file

@ -0,0 +1,3 @@
export { IconRail } from "./IconRail";
export { NavIcon } from "./NavIcon";
export { WorkspaceAvatar } from "./WorkspaceAvatar";