feat(ui): reconcile backup UI changes with current routing and interaction features

This commit is contained in:
Dotta
2026-03-02 16:44:03 -06:00
parent 83be94361c
commit 8ee063c4e5
69 changed files with 1591 additions and 666 deletions

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Paperclip, Plus } from "lucide-react";
import { useQueries } from "@tanstack/react-query";
import {
DndContext,
closestCenter,
@@ -18,6 +19,9 @@ import { CSS } from "@dnd-kit/utilities";
import { useCompany } from "../context/CompanyContext";
import { useDialog } from "../context/DialogContext";
import { cn } from "../lib/utils";
import { queryKeys } from "../lib/queryKeys";
import { sidebarBadgesApi } from "../api/sidebarBadges";
import { heartbeatsApi } from "../api/heartbeats";
import {
Tooltip,
TooltipContent,
@@ -65,10 +69,14 @@ function sortByStoredOrder(companies: Company[]): Company[] {
function SortableCompanyItem({
company,
isSelected,
hasLiveAgents,
hasUnreadInbox,
onSelect,
}: {
company: Company;
isSelected: boolean;
hasLiveAgents: boolean;
hasUnreadInbox: boolean;
onSelect: () => void;
}) {
const {
@@ -88,28 +96,28 @@ function SortableCompanyItem({
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<div ref={setNodeRef} style={style} {...attributes} {...listeners} className="overflow-visible">
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<a
href={`/dashboard?company=${company.id}`}
href={`/${company.issuePrefix}/dashboard`}
onClick={(e) => {
e.preventDefault();
onSelect();
}}
className="relative flex items-center justify-center group"
className="relative flex items-center justify-center group overflow-visible"
>
{/* Selection indicator pill */}
<div
className={cn(
"absolute left-[-14px] w-1 rounded-r-full bg-foreground transition-all duration-200",
"absolute left-[-14px] w-1 rounded-r-full bg-foreground transition-[height] duration-150",
isSelected
? "h-5"
: "h-0 group-hover:h-2"
)}
/>
<div
className={cn("transition-all duration-200", isDragging && "scale-105")}
className={cn("relative overflow-visible transition-transform duration-150", isDragging && "scale-105")}
>
<CompanyPatternIcon
companyName={company.name}
@@ -121,6 +129,17 @@ function SortableCompanyItem({
isDragging && "shadow-lg",
)}
/>
{hasLiveAgents && (
<span className="pointer-events-none absolute -right-0.5 -top-0.5 z-10">
<span className="relative flex h-2.5 w-2.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-80" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-blue-500 ring-2 ring-background" />
</span>
</span>
)}
{hasUnreadInbox && (
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 z-10 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-background" />
)}
</div>
</a>
</TooltipTrigger>
@@ -139,6 +158,36 @@ export function CompanyRail() {
() => companies.filter((company) => company.status !== "archived"),
[companies],
);
const companyIds = useMemo(() => sidebarCompanies.map((company) => company.id), [sidebarCompanies]);
const liveRunsQueries = useQueries({
queries: companyIds.map((companyId) => ({
queryKey: queryKeys.liveRuns(companyId),
queryFn: () => heartbeatsApi.liveRunsForCompany(companyId),
refetchInterval: 10_000,
})),
});
const sidebarBadgeQueries = useQueries({
queries: companyIds.map((companyId) => ({
queryKey: queryKeys.sidebarBadges(companyId),
queryFn: () => sidebarBadgesApi.get(companyId),
refetchInterval: 15_000,
})),
});
const hasLiveAgentsByCompanyId = useMemo(() => {
const result = new Map<string, boolean>();
companyIds.forEach((companyId, index) => {
result.set(companyId, (liveRunsQueries[index]?.data?.length ?? 0) > 0);
});
return result;
}, [companyIds, liveRunsQueries]);
const hasUnreadInboxByCompanyId = useMemo(() => {
const result = new Map<string, boolean>();
companyIds.forEach((companyId, index) => {
result.set(companyId, (sidebarBadgeQueries[index]?.data?.inbox ?? 0) > 0);
});
return result;
}, [companyIds, sidebarBadgeQueries]);
// Maintain sorted order in local state, synced from companies + localStorage
const [orderedIds, setOrderedIds] = useState<string[]>(() =>
@@ -219,7 +268,7 @@ export function CompanyRail() {
</div>
{/* Company list */}
<div className="flex-1 flex flex-col items-center gap-2 py-2 overflow-y-auto scrollbar-none">
<div className="flex-1 flex flex-col items-center gap-2 py-3 w-full overflow-y-auto overflow-x-hidden scrollbar-none">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
@@ -234,6 +283,8 @@ export function CompanyRail() {
key={company.id}
company={company}
isSelected={company.id === selectedCompanyId}
hasLiveAgents={hasLiveAgentsByCompanyId.get(company.id) ?? false}
hasUnreadInbox={hasUnreadInboxByCompanyId.get(company.id) ?? false}
onSelect={() => setSelectedCompanyId(company.id)}
/>
))}
@@ -250,7 +301,8 @@ export function CompanyRail() {
<TooltipTrigger asChild>
<button
onClick={() => openOnboarding()}
className="flex items-center justify-center w-11 h-11 rounded-[22px] hover:rounded-[14px] border-2 border-dashed border-border text-muted-foreground hover:border-foreground/30 hover:text-foreground transition-all duration-200"
className="flex items-center justify-center w-11 h-11 rounded-[22px] hover:rounded-[14px] border-2 border-dashed border-border text-muted-foreground hover:border-foreground/30 hover:text-foreground transition-[border-color,color,border-radius] duration-150"
aria-label="Add company"
>
<Plus className="h-5 w-5" />
</button>