Add the persistent sidebar and shared Pro upsell tiles

This commit is contained in:
Jonathan Singer
2026-07-20 12:02:10 -04:00
parent 5dee36d30a
commit 8d9f785dc7
3 changed files with 396 additions and 1 deletions
+148
View File
@@ -0,0 +1,148 @@
import React, { useState } from "react";
import { ArrowUpRight } from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
/**
* Shared Pro CTA primitives. Every Pro item is a direct link-out to the cloud
* sign-up in a new tab with a hover tooltip one-liner (no modal, no lock icon).
* Built once here and reused by the sidebar Platform section, the top upsell
* row, and the inline CTAs in the tabs.
*/
/** Small "Pro" pill. Deliberately not a padlock. */
export function ProTag({ className = "" }: { className?: string }) {
return (
<span
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-[#aaa] ${className}`}
style={{ border: "1px solid #2a2a2a", background: "rgba(255,255,255,0.04)" }}
>
Pro
</span>
);
}
/**
* Lightweight hover tooltip. Wraps a trigger and reveals `text` above it on
* hover/focus. Plain Tailwind + local state (no radix vendored).
*/
export function Tooltip({
text,
children,
className = "",
}: {
text: string;
children: React.ReactNode;
className?: string;
}) {
const [open, setOpen] = useState(false);
return (
<span
className={`relative inline-flex ${className}`}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
>
{children}
{open && (
<span
role="tooltip"
className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-max max-w-[240px] -translate-x-1/2 rounded-md px-2.5 py-1.5 text-xs text-[#ddd] shadow-lg"
style={{ border: "1px solid #2a2a2a", background: "#0a0a0a" }}
>
{text}
</span>
)}
</span>
);
}
export interface ProItem {
title: string;
desc: string;
slug: string;
icon: React.ElementType;
}
/**
* Card-style Pro feature tile: icon + name + one-liner + Pro tag + arrow.
* Used in the top upsell row and inline CTA grids.
*/
export function ProTile({ item }: { item: ProItem }) {
const Icon = item.icon;
return (
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(item.slug)}
title={item.desc}
className="group block cursor-pointer rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-4 text-left transition-colors hover:border-[#444]"
>
<div className="mb-2 flex items-center justify-between">
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
<div className="flex items-center gap-1.5">
<ProTag />
<ArrowUpRight className="h-3.5 w-3.5 text-[#555] transition-colors group-hover:text-[#aaa]" aria-hidden="true" />
</div>
</div>
<p className="text-sm font-medium text-white">{item.title}</p>
<p className="mt-0.5 text-xs text-[#666]">{item.desc}</p>
</a>
);
}
/**
* Sidebar-row Pro item: icon + label + Pro tag, with the one-liner as a hover
* tooltip. Link-out to sign-up in a new tab.
*/
export function ProNavItem({ item }: { item: ProItem }) {
const Icon = item.icon;
return (
<Tooltip text={item.desc} className="w-full">
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(item.slug)}
className="group flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm text-[#888] transition-colors hover:bg-[rgba(255,255,255,0.06)] hover:text-white"
>
<Icon className="h-4 w-4 flex-shrink-0 text-[#666] transition-colors group-hover:text-[#aaa]" aria-hidden="true" />
<span className="flex-1 truncate">{item.title}</span>
<ProTag />
</a>
</Tooltip>
);
}
/**
* Inline Pro CTA button (compact). Used in the finding detail and per-surface
* rows where a full card is too heavy.
*/
export function ProInlineCta({
label,
desc,
slug,
icon: Icon,
}: {
label: string;
desc: string;
slug: string;
icon: React.ElementType;
}) {
return (
<Tooltip text={desc}>
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(slug)}
className="group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
>
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
<span>{label}</span>
<ProTag className="ml-0.5" />
</a>
</Tooltip>
);
}
+218
View File
@@ -0,0 +1,218 @@
import React from "react";
import {
FileText,
Bug,
Waypoints,
History,
Mail,
GitPullRequest,
Search,
Layers,
Globe,
Puzzle,
Users,
LayoutDashboard,
AlertTriangle,
MessageSquare,
Network,
Database,
ArrowUpRight,
LogOut,
ShieldCheck,
} from "lucide-react";
import { SIGNUP_URL, trackCta } from "@/lib/cta";
import { ProNavItem, type ProItem } from "@/components/ProCta";
import type { View } from "@/App";
/**
* Persistent left rail. Mirrors the cloud app's nav: real in-page content
* ("This run"), local email-gated features ("Your runs"), and org-oriented
* Pro link-outs ("Platform"). Matches App.tsx's dark palette.
*/
// Org-signal items lead the list (PR Reviews, Repositories, Domains,
// Integrations, Members), then the rest of the platform surface.
const PLATFORM_ITEMS: ProItem[] = [
{ title: "PR Reviews", desc: "Pentest every pull request your team opens.", slug: "pr_reviews", icon: GitPullRequest },
{ title: "Repositories", desc: "Connect your org's repositories and domains.", slug: "repositories", icon: Layers },
{ title: "Domains", desc: "Connect your org's repositories and domains.", slug: "domains", icon: Globe },
{ title: "Integrations", desc: "Two-way sync findings to Jira, Linear, and Slack.", slug: "integrations", icon: Puzzle },
{ title: "Members", desc: "Invite your team and set roles.", slug: "members", icon: Users },
{ title: "Pentests", desc: "Launch deeper pentests on managed infra.", slug: "pentests", icon: Search },
{ title: "Dashboard", desc: "See every project and finding in one place.", slug: "dashboard", icon: LayoutDashboard },
{ title: "Issues", desc: "Track and triage findings across your org.", slug: "platform_issues", icon: AlertTriangle },
{ title: "Chat", desc: "Ask the agents about any finding.", slug: "chat", icon: MessageSquare },
{ title: "Networks", desc: "Map and monitor your network exposure.", slug: "networks", icon: Network },
{ title: "Knowledge", desc: "Give agents context about your systems.", slug: "knowledge", icon: Database },
];
interface SidebarProps {
view: View;
onSelectView: (view: View) => void;
issuesCount: number;
agentCount: number;
runCount: number;
verified: boolean;
email: string | null;
onOpenEmail: () => void;
onOpenHistory: () => void;
onForget: () => void;
}
export default function Sidebar({
view,
onSelectView,
issuesCount,
agentCount,
runCount,
verified,
email,
onOpenEmail,
onOpenHistory,
onForget,
}: SidebarProps) {
return (
<aside className="hidden w-60 flex-shrink-0 border-r border-[#222] lg:block">
<div className="sticky top-0 flex h-screen flex-col overflow-y-auto px-3 py-4">
{/* Header: wordmark + Start free + signed-in chip */}
<div className="px-1.5">
<a
href="https://app.strix.ai"
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("logo")}
className="flex items-center gap-1.5 opacity-90 transition-opacity hover:opacity-100"
title="Open Strix Cloud"
>
<img src="./logo.png" alt="Strix" className="h-8 w-10 object-cover" />
<span className="text-base font-medium tracking-tight text-white">Strix</span>
</a>
<a
href={SIGNUP_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta("sidebar_start_free")}
className="mt-3 flex w-full cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-white px-3 py-2 text-sm font-semibold text-black transition-opacity hover:opacity-90"
>
Start free
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
{verified && email && (
<div
className="mt-2.5 flex items-center gap-2 rounded-lg px-2.5 py-2"
style={{ border: "1px solid #222", background: "rgba(255,255,255,0.02)" }}
>
<ShieldCheck className="h-3.5 w-3.5 flex-shrink-0 text-emerald-400" aria-hidden="true" />
<div className="min-w-0 flex-1">
<p className="truncate text-[11px] text-[#666]">Signed in as</p>
<p className="truncate text-xs text-[#aaa]" title={email}>{email}</p>
</div>
<button
onClick={onForget}
title="Forget this email on this machine"
className="flex-shrink-0 cursor-pointer text-[#666] transition-colors hover:text-white"
aria-label="Forget"
>
<LogOut className="h-3.5 w-3.5" />
</button>
</div>
)}
</div>
{/* This run */}
<Section label="This run">
<NavItem
icon={FileText}
label="Overview"
active={view === "overview"}
onClick={() => onSelectView("overview")}
/>
<NavItem
icon={Bug}
label="Issues"
count={issuesCount > 0 ? issuesCount : undefined}
active={view === "issues"}
onClick={() => onSelectView("issues")}
/>
{agentCount > 0 && (
<NavItem
icon={Waypoints}
label="Agents"
count={agentCount}
active={view === "agents"}
onClick={() => onSelectView("agents")}
/>
)}
</Section>
{/* Your runs (local, unlock with email) */}
<Section label="Your runs">
<NavItem
icon={History}
label="Past runs"
count={runCount > 0 ? runCount : undefined}
active={view === "history"}
onClick={onOpenHistory}
/>
<NavItem icon={Mail} label="Email report" onClick={onOpenEmail} />
</Section>
{/* Platform (Pro link-outs) */}
<div className="mt-6">
<p className="px-2.5 pb-1 text-[11px] font-semibold uppercase tracking-wide text-[#555]">
Platform
</p>
<p className="px-2.5 pb-2 text-[11px] leading-snug text-[#666]">
Get the most out of Strix for your team.
</p>
<div className="space-y-0.5">
{PLATFORM_ITEMS.map((item) => (
<ProNavItem key={item.slug} item={item} />
))}
</div>
</div>
</div>
</aside>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="mt-6">
<p className="px-2.5 pb-1.5 text-[11px] font-semibold uppercase tracking-wide text-[#555]">
{label}
</p>
<div className="space-y-0.5">{children}</div>
</div>
);
}
function NavItem({
icon: Icon,
label,
count,
active,
onClick,
}: {
icon: React.ElementType;
label: string;
count?: number;
active?: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors ${
active
? "text-white"
: "text-[#888] hover:bg-[rgba(255,255,255,0.06)] hover:text-white"
}`}
style={active ? { background: "rgba(255,255,255,0.12)" } : undefined}
>
<Icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
<span className="flex-1 text-left">{label}</span>
{count != null && <span className="text-xs text-[#666] tabular-nums">{count}</span>}
</button>
);
}
@@ -1,7 +1,8 @@
"use client";
import React, { useState } from "react";
import { Clock, CheckCircle2, Ban, History, BellOff } from "lucide-react";
import { Clock, CheckCircle2, Ban, History, BellOff, Wrench, GitMerge, GitPullRequest } from "lucide-react";
import { ProInlineCta } from "@/components/ProCta";
import { Vulnerability, VulnerabilityStatus, SEVERITY_COLORS, STATUS_META, isSeverityOverridden } from "@/types/issues";
import { formatTimeAgo } from "@/lib/utils";
import { getSeverityDot } from "@/lib/vulnerability-utils";
@@ -228,6 +229,34 @@ export default function VulnerabilityDetail({ vulnerability }: VulnerabilityDeta
/>
</div>
</div>
{/* Team-workflow CTAs (Pro). Highest-intent surface: act on this finding. */}
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
<p className="text-sm font-semibold text-white">Ship the fix with your team</p>
<p className="mt-0.5 text-xs text-[#666]">
Take this finding into your team's workflow.
</p>
<div className="mt-3 flex flex-wrap gap-2.5">
<ProInlineCta
label="Auto-fix & open a PR"
desc="Fix it for you and open a PR, retested."
slug="autofix"
icon={Wrench}
/>
<ProInlineCta
label="Sync to Jira / Linear"
desc="Two-way sync findings to Jira, Linear, and Slack."
slug="integrations"
icon={GitMerge}
/>
<ProInlineCta
label="Catch this in PR reviews"
desc="Pentest every pull request your team opens."
slug="pr_reviews"
icon={GitPullRequest}
/>
</div>
</div>
</div>
);
}