223 lines
7.5 KiB
TypeScript
223 lines
7.5 KiB
TypeScript
import { Link, useLocation, useParams } from "react-router-dom";
|
|
import { ChevronRight, Home, Settings } from "lucide-react";
|
|
import { useProjectLayout } from "@/app/project/layout";
|
|
import { useProjectsQuery } from "@/hooks/useProjectsQuery";
|
|
import { useOptionalRoom } from "@/contexts/room";
|
|
import { useState } from "react";
|
|
import { RoomSettingsModal } from "@/app/project/channel/RoomSettingsModal";
|
|
|
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
function truncateUuid(s: string): string {
|
|
return s.slice(0, 8);
|
|
}
|
|
|
|
interface BreadcrumbSegment {
|
|
label: string;
|
|
path: string;
|
|
isLast: boolean;
|
|
fullUuid?: string;
|
|
}
|
|
|
|
function useBreadcrumbs(): BreadcrumbSegment[] {
|
|
const location = useLocation();
|
|
const params = useParams<{
|
|
projectName?: string;
|
|
repoName?: string;
|
|
issueNumber?: string;
|
|
skillSlug?: string;
|
|
roomId?: string;
|
|
}>();
|
|
const roomContext = useOptionalRoom();
|
|
const currentRoom = roomContext?.currentRoom;
|
|
|
|
const { data: projects = [] } = useProjectsQuery();
|
|
const activeProject = projects.find((p) => p.name === params.projectName);
|
|
|
|
const segments: BreadcrumbSegment[] = [];
|
|
const pathParts = location.pathname.split("/").filter(Boolean);
|
|
|
|
for (let i = 0; i < pathParts.length; i++) {
|
|
const part = pathParts[i];
|
|
const path = "/" + pathParts.slice(0, i + 1).join("/");
|
|
const isLast = i === pathParts.length - 1;
|
|
|
|
let label = part;
|
|
let fullUuid: string | undefined;
|
|
|
|
if (UUID_RE.test(part)) {
|
|
fullUuid = part;
|
|
label = truncateUuid(part);
|
|
} else if (part === params.projectName && activeProject) {
|
|
label = activeProject.display_name;
|
|
} else if (part === params.repoName) {
|
|
label = part;
|
|
} else if (part === params.issueNumber) {
|
|
label = `#${part}`;
|
|
} else if (part === params.skillSlug) {
|
|
label = part;
|
|
} else if (part === "channel" && params.roomId && currentRoom) {
|
|
label = currentRoom.room_name;
|
|
} else {
|
|
label = part.charAt(0).toUpperCase() + part.slice(1);
|
|
}
|
|
|
|
segments.push({ label, path, isLast, fullUuid });
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
const TOOLBAR_ICONS = [
|
|
{
|
|
label: "Pinned Messages",
|
|
path: "M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z",
|
|
},
|
|
{
|
|
label: "Search",
|
|
path: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
|
|
},
|
|
{
|
|
label: "Notifications",
|
|
path: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
|
|
},
|
|
{
|
|
label: "Help",
|
|
path: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
},
|
|
];
|
|
|
|
export function Header() {
|
|
const location = useLocation();
|
|
const segments = useBreadcrumbs();
|
|
const { showMembers, setShowMembers } = useProjectLayout();
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
const roomContext = useOptionalRoom();
|
|
|
|
const handleCopy = (e: React.MouseEvent, text: string) => {
|
|
e.preventDefault();
|
|
navigator.clipboard.writeText(text);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<header
|
|
className="h-12 flex items-center justify-between px-4 shrink-0"
|
|
style={{
|
|
borderBottom: "1px solid var(--border-subtle)",
|
|
backgroundColor: "var(--surface-ground)",
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-1 min-w-0 text-sm">
|
|
<Link
|
|
to="/me"
|
|
className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
|
>
|
|
<Home className="w-3.5 h-3.5" />
|
|
</Link>
|
|
|
|
{segments.map((segment, idx) => (
|
|
<span key={segment.path + idx} className="flex items-center gap-1">
|
|
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground/50 shrink-0" />
|
|
{segment.isLast ? (
|
|
<span
|
|
className={`font-medium text-foreground truncate${segment.fullUuid ? " font-mono cursor-pointer hover:text-accent transition-colors" : ""}`}
|
|
title={segment.fullUuid}
|
|
{...(segment.fullUuid
|
|
? { onClick: (e: React.MouseEvent) => handleCopy(e, segment.fullUuid!) }
|
|
: {})}
|
|
>
|
|
{segment.label}
|
|
</span>
|
|
) : segment.fullUuid ? (
|
|
<Link
|
|
to={segment.path}
|
|
className="text-muted-foreground hover:text-foreground font-mono cursor-pointer transition-colors"
|
|
title={segment.fullUuid}
|
|
onClick={(e) => handleCopy(e, segment.fullUuid!)}
|
|
>
|
|
{segment.label}
|
|
</Link>
|
|
) : (
|
|
<Link
|
|
to={segment.path}
|
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
{segment.label}
|
|
</Link>
|
|
)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
{location.pathname.startsWith("/me") ? null : (
|
|
<>
|
|
{roomContext?.currentRoom && location.pathname.includes("/channel/") && (
|
|
<button
|
|
onClick={() => setShowSettings(true)}
|
|
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors hover:bg-hover-bg"
|
|
style={{ color: "var(--text-secondary)" }}
|
|
title="Room Settings"
|
|
>
|
|
<Settings className="w-[18px] h-[18px]" />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => setShowMembers(!showMembers)}
|
|
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
|
|
style={
|
|
showMembers
|
|
? {
|
|
color: "var(--text-primary)",
|
|
backgroundColor: "var(--hover-bg-strong)",
|
|
}
|
|
: { color: "var(--text-secondary)" }
|
|
}
|
|
>
|
|
<svg
|
|
className="w-[18px] h-[18px]"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={1.5}
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
{TOOLBAR_ICONS.map((icon, i) => (
|
|
<button
|
|
key={i}
|
|
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
|
|
style={{ color: "var(--text-secondary)" }}
|
|
title={icon.label}
|
|
>
|
|
<svg
|
|
className="w-[18px] h-[18px]"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={1.5}
|
|
d={icon.path}
|
|
/>
|
|
</svg>
|
|
</button>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
</header>
|
|
<RoomSettingsModal open={showSettings} onOpenChange={setShowSettings} />
|
|
</>
|
|
);
|
|
}
|