T-HUB/apps/web/src/components/shell/sidebar.tsx
hacklab c423f172d0 style: light theme and hand-inked logo matching acampada26s.net
The previous pass kept the dark base and the red icons, which did not
read as the acampada. Now:

- Light theme: white page, black ink and borders, black buttons with a
  hard neon offset shadow, neon for focus and the active nav item;
  h1/h2 in Anton with the stepped neon shadow of the site headings.
- Nav, empty-state, spinner and pattern assets in black; platform icons
  keep their dark disc with a neon stroke.
- New mark: three dome tents wired from their doors into a switch with
  an antenna, drawn in the inked style of the acampada logo (curved
  poles, ball ends, hatching) over a neon offset copy. LogoMark renders
  it inline; visible product name is T-HUB.
- docs/brand: t-hub.svg and a lockup with the Anton wordmark converted
  to outlines so it renders without the font.
- Public submission page moved to the light palette.

Checked with headless Chromium screenshots (offline sample data) on
desktop and mobile.
2026-10-15 14:40:02 +02:00

99 lines
4.6 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "../../lib/auth-store";
import { useBranding } from "../../lib/branding";
import { useApiResource } from "../../lib/use-api";
import type { DraftRecord } from "../../lib/api-types";
import { LogoMark } from "../brand/logo";
import { NavIcon } from "../brand";
const NAV: { href: string; label: string; icon: string; external?: boolean }[] = [
{ href: "/streams", label: "Streams", icon: "video" },
{ href: "/control-room", label: "Control Room", icon: "video" },
{ href: "/compose", label: "Compose", icon: "compose" },
{ href: "/calendar", label: "Calendar", icon: "calendar" },
{ href: "/chat", label: "Chat", icon: "chat" },
{ href: "/files", label: "Files", icon: "files" },
{ href: "/archive", label: "Archive", icon: "files" },
{ href: "/inbox", label: "Inbox", icon: "inbox" },
{ href: "/outreach", label: "Outreach", icon: "team" },
{ href: "/approvals", label: "Approvals", icon: "approvals" },
{ href: "/video", label: "Video", icon: "video" },
{ href: "/analytics", label: "Analytics", icon: "analytics" },
{ href: "/connections", label: "Channels", icon: "connections" },
{ href: "/team", label: "Team", icon: "team" },
{ href: "/audit", label: "Audit log", icon: "audit" },
{ href: "/onboarding", label: "Onboarding", icon: "home" },
{ href: "/settings", label: "Settings", icon: "settings" },
];
const identity = (drafts: DraftRecord[]): DraftRecord[] => drafts;
const emptyDrafts: DraftRecord[] = [];
export function Sidebar(): React.ReactElement {
const pathname = usePathname();
const { session, can } = useAuth();
const branding = useBranding();
const drafts = useApiResource<DraftRecord[], DraftRecord[]>("/publishing/drafts", identity, emptyDrafts, { state: "in_review", limit: 100 });
// Real in-review count from the API; hidden while loading/offline and for non-approvers.
const approvals = can("approval.grant") && drafts.source === "live" ? drafts.data.length : 0;
// Branding is per collective; the session's workspace is the source of truth
// for the name, branding only decorates it (and may be loading/unavailable).
const workspaceName = session?.workspace.name ?? branding.workspaceName ?? "Studio";
const showTagline = branding.source === "live" && branding.tagline !== null;
return (
<aside className="hidden h-screen w-52 shrink-0 flex-col border-r border-border-subtle bg-surface lg:flex">
<div className="flex items-start gap-2.5 border-b border-border-subtle px-4 py-4">
{branding.logoUrl !== null ? (
<img
src={branding.logoUrl}
alt={`${workspaceName} logo`}
width={30}
height={30}
className="mt-0.5 h-[30px] w-[30px] shrink-0 rounded object-contain"
/>
) : (
<LogoMark size={30} />
)}
<div className="min-w-0 flex-1">
<div className="text-xs font-semibold leading-tight tracking-tight" title={workspaceName}>
{workspaceName}
</div>
{showTagline ? (
<div className="mt-0.5 truncate text-2xs leading-snug text-text-faint" title={branding.tagline ?? undefined}>
{branding.tagline}
</div>
) : (
<div className="truncate text-2xs text-text-faint" title={workspaceName}>
T-HUB
</div>
)}
</div>
</div>
<nav className="flex-1 overflow-y-auto scrollbar-thin p-2">
{NAV.map((item) => {
const active = !item.external && pathname.startsWith(item.href);
const className = `group flex items-center justify-between rounded-md px-3 py-1.5 text-sm transition-colors duration-150 ${item.href === "/control-room" ? "min-h-11" : ""} ${
active ? "bg-accent-dim text-accent" : "text-text-dim hover:bg-surface-2 hover:text-text"
}`;
const icon = <NavIcon name={item.icon} size={16} />;
return item.external ? (
<a key={item.href} href={item.href} target="_blank" rel="noreferrer" className={className}>
<span className="flex items-center gap-2.5">{icon}{item.label}</span>
<span className="text-2xs text-text-faint">↗</span>
</a>
) : (
<Link key={item.href} href={item.href} className={className}>
<span className="flex items-center gap-2.5">{icon}{item.label}</span>
{item.href === "/approvals" && approvals > 0 && (
<span className="text-2xs text-text-faint">{approvals}</span>
)}
</Link>
);
})}
</nav>
</aside>
);
}