mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 17:27:26 +02:00
Add the persistent sidebar and shared Pro upsell tiles
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user