mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-08 15:22:39 +02:00
feat: add new layout system (Slack/ClickUp inspired)
This commit is contained in:
parent
2fd38615e8
commit
a919f8d9ee
28 changed files with 3059 additions and 0 deletions
60
surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
Normal file
60
surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
surfsense_web/components/layout/ui/icon-rail/NavIcon.tsx
Normal file
34
surfsense_web/components/layout/ui/icon-rail/NavIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
3
surfsense_web/components/layout/ui/icon-rail/index.ts
Normal file
3
surfsense_web/components/layout/ui/icon-rail/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { IconRail } from "./IconRail";
|
||||
export { NavIcon } from "./NavIcon";
|
||||
export { WorkspaceAvatar } from "./WorkspaceAvatar";
|
||||
Loading…
Add table
Add a link
Reference in a new issue