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.
99 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
}
|