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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
|
|
);
|
|
}
|