# AmroUI sales

Installable AmroUI product component source.

## Installation

```bash
npx shadcn@latest add https://amro-ui.vercel.app/r/amro-ui-sales.json
```

[Registry JSON](https://amro-ui.vercel.app/r/amro-ui-sales.json)

## Source

### source/components/sales.tsx

```tsx
import {
  Building2,
  Check,
  ChevronDown,
  CircleAlert,
  MapPin,
  Radar,
  Search,
  ShieldCheck,
  Sparkles,
  Target,
  UserRoundSearch,
  X
} from "lucide-react";
import {
  useId,
  useMemo,
  useState,
  type CSSProperties,
  type HTMLAttributes,
  type ReactNode
} from "react";
import { cn } from "../lib/cn.js";
import { Avatar } from "./avatar.js";
import { Badge } from "./badge.js";
import { Button } from "./button.js";
import { Card } from "./card.js";
import { EmptyNotice, FeaturePanel, MetricGrid, StatusIndicator } from "./feature-primitives.js";
import { Input, Textarea, TimeInput } from "./input.js";

function getAccessibleText(value: ReactNode, fallback: string): string {
  return typeof value === "string" || typeof value === "number" ? String(value) : fallback;
}

export interface SalesBrief {
  product: string;
  audience: string;
  territory: string;
  personas: string;
  signals: string;
}

export interface SalesBriefComposerProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<SalesBrief>;
  onSubmit?: (brief: SalesBrief) => void;
  submitLabel?: ReactNode;
}

export function SalesBriefComposer({
  className,
  defaultValue,
  onSubmit,
  submitLabel = "Build target audience",
  ...props
}: SalesBriefComposerProps) {
  const [brief, setBrief] = useState<SalesBrief>({
    product: defaultValue?.product ?? "AI customer operations platform",
    audience: defaultValue?.audience ?? "B2B SaaS companies with growing support teams",
    territory: defaultValue?.territory ?? "United Kingdom",
    personas: defaultValue?.personas ?? "VP Customer Success, Head of Support",
    signals: defaultValue?.signals ?? "Hiring support roles, recently funded"
  });
  const update = (field: keyof SalesBrief, value: string) =>
    setBrief((current) => ({ ...current, [field]: value }));
  const ready = [
    brief.product,
    brief.audience,
    brief.territory,
    brief.personas,
    brief.signals
  ].every((value) => value.trim().length > 1);
  return (
    <form
      className={cn("amro-sales-brief", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (ready) onSubmit?.(brief);
      }}
      {...props}
    >
      <FeaturePanel
        eyebrow="Campaign brief"
        title="Who should AmroGen find?"
        description="Define the offer and evidence the agent may use."
        status={
          <StatusIndicator
            label={ready ? "Ready" : "Incomplete"}
            status={ready ? "success" : "warning"}
          />
        }
      >
        <div className="amro-sales-brief__grid">
          <Input
            label="Product or offer"
            value={brief.product}
            onChange={(event) => update("product", event.target.value)}
          />
          <Input
            label="Target audience"
            value={brief.audience}
            onChange={(event) => update("audience", event.target.value)}
          />
          <Input
            label="Territory"
            value={brief.territory}
            onChange={(event) => update("territory", event.target.value)}
          />
          <Input
            label="Buyer personas"
            value={brief.personas}
            onChange={(event) => update("personas", event.target.value)}
          />
          <Input
            label="Buying signals"
            value={brief.signals}
            onChange={(event) => update("signals", event.target.value)}
          />
        </div>
        <div className="amro-sales-brief__footer">
          <span>
            <Radar aria-hidden="true" /> Research uses public and approved sources only
          </span>
          <Button disabled={!ready} type="submit">
            {submitLabel}
            <Sparkles aria-hidden="true" />
          </Button>
        </div>
      </FeaturePanel>
    </form>
  );
}

export interface ICPCriterion {
  id: string;
  label: string;
  group: "company" | "buyer" | "signal";
}
export interface ICPBuilderProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  criteria?: readonly ICPCriterion[];
  onChange?: (criteria: readonly ICPCriterion[]) => void;
  onSave?: (criteria: readonly ICPCriterion[]) => void;
}

const defaultCriteria: readonly ICPCriterion[] = [
  { id: "size", label: "50–500 employees", group: "company" },
  { id: "industry", label: "B2B software", group: "company" },
  { id: "persona", label: "Customer success leader", group: "buyer" },
  { id: "hiring", label: "Hiring support roles", group: "signal" }
];

export function ICPBuilder({
  className,
  criteria = defaultCriteria,
  onChange,
  onSave,
  ...props
}: ICPBuilderProps) {
  const [items, setItems] = useState([...criteria]);
  const [draft, setDraft] = useState("");
  const remove = (id: string) => {
    const next = items.filter((item) => item.id !== id);
    setItems(next);
    onChange?.(next);
  };
  const add = () => {
    const label = draft.trim();
    if (!label) return;
    const next = [...items, { id: `${Date.now()}`, label, group: "company" as const }];
    setItems(next);
    setDraft("");
    onChange?.(next);
  };
  return (
    <FeaturePanel
      className={cn("amro-icp-builder", className)}
      eyebrow="Ideal customer profile"
      title="Qualification criteria"
      description="Every criterion remains visible and editable."
      actions={
        <Button onClick={() => onSave?.(items)} size="sm">
          Save ICP
        </Button>
      }
      {...props}
    >
      {(["company", "buyer", "signal"] as const).map((group) => (
        <section key={group}>
          <h3>
            {group === "company" ? "Company" : group === "buyer" ? "Buyer" : "Buying signals"}
          </h3>
          <div>
            {items
              .filter((item) => item.group === group)
              .map((item) => (
                <span key={item.id}>
                  {item.label}
                  <button
                    aria-label={`Remove ${item.label}`}
                    onClick={() => remove(item.id)}
                    type="button"
                  >
                    <X aria-hidden="true" />
                  </button>
                </span>
              ))}
          </div>
        </section>
      ))}
      <div className="amro-icp-builder__add">
        <Input
          aria-label="Add criterion"
          placeholder="Add a company criterion"
          value={draft}
          onChange={(event) => setDraft(event.target.value)}
        />
        <Button disabled={!draft.trim()} onClick={add} variant="secondary">
          Add criterion
        </Button>
      </div>
    </FeaturePanel>
  );
}

export interface DetectedCompany {
  name: string;
  domain: string;
  industry?: string;
  location?: string;
}
export interface NamedAccountInputProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
  detectedCompany?: DetectedCompany;
  loading?: boolean;
  error?: ReactNode;
  onDetect?: (query: string) => void;
  onConfirm?: (company: DetectedCompany) => void;
}

export function NamedAccountInput({
  className,
  detectedCompany,
  error,
  loading = false,
  onConfirm,
  onDetect,
  ...props
}: NamedAccountInputProps) {
  const [query, setQuery] = useState("");
  return (
    <form
      className={cn("amro-named-account", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (query.trim()) onDetect?.(query.trim());
      }}
      {...props}
    >
      <Input
        error={error}
        hint="Paste a company website or enter its name."
        label="Named account"
        onChange={(event) => setQuery(event.target.value)}
        placeholder="northstar.example"
        value={query}
      />
      <Button disabled={!query.trim() || loading} type="submit">
        {loading ? "Detecting…" : "Detect company"}
        <Search aria-hidden="true" />
      </Button>
      {detectedCompany && (
        <Card>
          <Building2 aria-hidden="true" />
          <span>
            <strong>{detectedCompany.name}</strong>
            <small>
              {detectedCompany.domain}
              {detectedCompany.industry ? ` · ${detectedCompany.industry}` : ""}
              {detectedCompany.location ? ` · ${detectedCompany.location}` : ""}
            </small>
          </span>
          <Badge tone="success">
            <Check aria-hidden="true" /> Match found
          </Badge>
          <Button onClick={() => onConfirm?.(detectedCompany)} size="sm">
            Use company
          </Button>
        </Card>
      )}
    </form>
  );
}

export type AudienceSignalType = "hiring" | "funding" | "technology" | "intent" | "competitor";
export interface AudienceSignalChipProps extends HTMLAttributes<HTMLSpanElement> {
  type?: AudienceSignalType;
  strength?: "weak" | "medium" | "strong";
  selected?: boolean;
  onSelectedChange?: (selected: boolean) => void;
}
export function AudienceSignalChip({
  children,
  className,
  onSelectedChange,
  selected = false,
  strength = "strong",
  type = "intent",
  ...props
}: AudienceSignalChipProps) {
  return (
    <span
      className={cn("amro-audience-signal", `amro-audience-signal--${strength}`, className)}
      data-selected={selected}
      {...props}
    >
      <button aria-pressed={selected} onClick={() => onSelectedChange?.(!selected)} type="button">
        <Radar aria-hidden="true" />
        <span>{children ?? `${type[0]?.toUpperCase()}${type.slice(1)} signal`}</span>
        <small>{strength}</small>
      </button>
    </span>
  );
}

export interface ProspectCompany {
  id: string;
  name: string;
  domain?: string;
  industry?: string;
  location?: string;
  fitScore: number;
  reasons?: readonly string[];
  logoUrl?: string;
}
export interface ProspectCompanyCardProps extends HTMLAttributes<HTMLDivElement> {
  company?: ProspectCompany;
  selected?: boolean;
  onSelectedChange?: (selected: boolean) => void;
  onOpen?: (company: ProspectCompany) => void;
}
const sampleCompany: ProspectCompany = {
  id: "northstar",
  name: "Northstar Labs",
  domain: "northstar.example",
  industry: "B2B software",
  location: "London, UK",
  fitScore: 92,
  reasons: ["Hiring support leaders", "Uses compatible CRM", "Series B funding"]
};
export function ProspectCompanyCard({
  className,
  company = sampleCompany,
  onOpen,
  onSelectedChange,
  selected = false,
  ...props
}: ProspectCompanyCardProps) {
  return (
    <Card
      className={cn(
        "amro-prospect-company",
        selected && "amro-prospect-company--selected",
        className
      )}
      {...props}
    >
      <header>
        <Avatar name={company.name} {...(company.logoUrl ? { src: company.logoUrl } : {})} />
        <span>
          <strong>{company.name}</strong>
          <small>{company.domain}</small>
        </span>
        <label>
          <input
            checked={selected}
            onChange={(event) => onSelectedChange?.(event.target.checked)}
            type="checkbox"
          />
          <span className="amro-sr-only">Select {company.name}</span>
        </label>
      </header>
      <div className="amro-prospect-company__meta">
        <span>
          <Building2 aria-hidden="true" /> {company.industry}
        </span>
        <span>
          <MapPin aria-hidden="true" /> {company.location}
        </span>
      </div>
      <div className="amro-prospect-company__fit">
        <span>
          <strong>{company.fitScore}</strong>
          <small>/100 fit</small>
        </span>
        <meter max="100" min="0" value={company.fitScore}>
          {company.fitScore}%
        </meter>
      </div>
      <ul>
        {company.reasons?.map((reason) => (
          <li key={reason}>
            <Check aria-hidden="true" /> {reason}
          </li>
        ))}
      </ul>
      {onOpen && (
        <Button onClick={() => onOpen(company)} variant="tertiary">
          Review evidence
        </Button>
      )}
    </Card>
  );
}

export interface DecisionMaker {
  id: string;
  name: string;
  title: string;
  company: string;
  avatarUrl?: string;
  contactStatus?: "verified" | "unverified" | "unavailable";
  relevance?: string;
}
export interface DecisionMakerCardProps extends HTMLAttributes<HTMLDivElement> {
  person?: DecisionMaker;
  onOpen?: (person: DecisionMaker) => void;
  onAdd?: (person: DecisionMaker) => void;
}
const samplePerson: DecisionMaker = {
  id: "maya",
  name: "Maya Chen",
  title: "VP Customer Success",
  company: "Northstar Labs",
  contactStatus: "verified",
  relevance: "Owns support efficiency and is hiring three customer-success roles."
};
export function DecisionMakerCard({
  className,
  onAdd,
  onOpen,
  person = samplePerson,
  ...props
}: DecisionMakerCardProps) {
  return (
    <Card className={cn("amro-decision-maker", className)} {...props}>
      <header>
        <Avatar name={person.name} {...(person.avatarUrl ? { src: person.avatarUrl } : {})} />
        <span>
          <strong>{person.name}</strong>
          <small>
            {person.title} · {person.company}
          </small>
        </span>
        <StatusIndicator
          label={
            person.contactStatus === "verified"
              ? "Contact verified"
              : (person.contactStatus ?? "unverified")
          }
          status={person.contactStatus === "verified" ? "success" : "warning"}
        />
      </header>
      <p>{person.relevance}</p>
      <footer>
        {onOpen && (
          <Button onClick={() => onOpen(person)} variant="tertiary">
            View profile
          </Button>
        )}
        {onAdd && (
          <Button onClick={() => onAdd(person)} size="sm">
            <UserRoundSearch aria-hidden="true" /> Add prospect
          </Button>
        )}
      </footer>
    </Card>
  );
}

export interface ProspectStackProps extends HTMLAttributes<HTMLDivElement> {
  prospects?: readonly ProspectCompany[];
  initialIndex?: number;
  onReview?: (company: ProspectCompany) => void;
  onDismiss?: (company: ProspectCompany) => void;
}
export function ProspectStack({
  className,
  initialIndex = 0,
  onDismiss,
  onReview,
  prospects = [
    sampleCompany,
    { ...sampleCompany, id: "atlas", name: "Atlas CX", fitScore: 86 },
    { ...sampleCompany, id: "orbit", name: "Orbit Desk", fitScore: 78 }
  ],
  ...props
}: ProspectStackProps) {
  const [index, setIndex] = useState(initialIndex);
  const current = prospects[index];
  if (!current)
    return (
      <EmptyNotice
        className={className}
        title="Prospect review complete"
        description="Every matched account has been triaged."
      />
    );
  const next = () => setIndex((value) => value + 1);
  return (
    <div className={cn("amro-prospect-stack", className)} {...props}>
      <div className="amro-prospect-stack__depth" aria-hidden="true" />
      <ProspectCompanyCard company={current} />
      <footer>
        <span>
          {index + 1} of {prospects.length}
        </span>
        <Button
          onClick={() => {
            onDismiss?.(current);
            next();
          }}
          variant="secondary"
        >
          Skip
        </Button>
        <Button
          onClick={() => {
            onReview?.(current);
            next();
          }}
        >
          Review prospect
        </Button>
      </footer>
    </div>
  );
}

export interface FitEvidence {
  id: string;
  label: ReactNode;
  score: number;
  detail?: ReactNode;
}
export interface LeadFitMeterProps extends HTMLAttributes<HTMLDivElement> {
  score?: number;
  evidence?: readonly FitEvidence[];
  label?: ReactNode;
}
const fitEvidence: readonly FitEvidence[] = [
  { id: "company", label: "Company profile", score: 96, detail: "Industry and size match" },
  { id: "buyer", label: "Buyer relevance", score: 91, detail: "Owns the problem" },
  { id: "signal", label: "Buying signals", score: 84, detail: "2 current signals" }
];
export function LeadFitMeter({
  className,
  evidence = fitEvidence,
  label = "Lead fit",
  score = 92,
  ...props
}: LeadFitMeterProps) {
  const [expanded, setExpanded] = useState(false);
  return (
    <div className={cn("amro-lead-fit", className)} {...props}>
      <button aria-expanded={expanded} onClick={() => setExpanded((value) => !value)} type="button">
        <span
          className="amro-lead-fit__ring"
          style={
            { "--amro-fit-score": `${Math.max(0, Math.min(score, 100)) * 3.6}deg` } as CSSProperties
          }
        >
          <strong>{score}</strong>
          <small>/100</small>
        </span>
        <span>
          <strong>{label}</strong>
          <small>
            {score >= 85 ? "Strong match" : score >= 65 ? "Potential match" : "Review required"}
          </small>
        </span>
        <ChevronDown aria-hidden="true" />
      </button>
      {expanded && (
        <ul>
          {evidence.map((item) => (
            <li key={item.id}>
              <span>
                <strong>{item.label}</strong>
                {item.detail && <small>{item.detail}</small>}
              </span>
              <meter max="100" value={item.score}>
                {item.score}%
              </meter>
              <strong>{item.score}</strong>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export interface FitReasonPopoverProps extends HTMLAttributes<HTMLDivElement> {
  reasons?: readonly FitEvidence[];
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  triggerLabel?: ReactNode;
}
export function FitReasonPopover({
  className,
  onOpenChange,
  open: controlledOpen,
  reasons = fitEvidence,
  triggerLabel = "Why this prospect?",
  ...props
}: FitReasonPopoverProps) {
  const [internalOpen, setInternalOpen] = useState(false);
  const open = controlledOpen ?? internalOpen;
  const id = useId();
  const toggle = () => {
    setInternalOpen(!open);
    onOpenChange?.(!open);
  };
  return (
    <div className={cn("amro-fit-popover", className)} {...props}>
      <Button aria-controls={id} aria-expanded={open} onClick={toggle} variant="tertiary">
        <Target aria-hidden="true" /> {triggerLabel}
      </Button>
      {open && (
        <div id={id} role="dialog" aria-label="Prospect fit evidence">
          <header>
            <strong>Selection evidence</strong>
            <button aria-label="Close evidence" onClick={toggle} type="button">
              <X aria-hidden="true" />
            </button>
          </header>
          <ul>
            {reasons.map((reason) => (
              <li key={reason.id}>
                <Check aria-hidden="true" />
                <span>
                  <strong>{reason.label}</strong>
                  <small>{reason.detail}</small>
                </span>
                <Badge tone="neutral">{reason.score}/100</Badge>
              </li>
            ))}
          </ul>
          <small>Score inputs are visible so your team can challenge the recommendation.</small>
        </div>
      )}
    </div>
  );
}

export interface CampaignOutcomeBoardProps extends HTMLAttributes<HTMLDivElement> {
  businesses?: number;
  people?: number;
  messages?: number;
  approved?: number;
  lastUpdated?: ReactNode;
}
export function CampaignOutcomeBoard({
  approved = 18,
  businesses = 42,
  className,
  lastUpdated = "Updated just now",
  messages = 31,
  people = 68,
  ...props
}: CampaignOutcomeBoardProps) {
  return (
    <FeaturePanel
      className={cn("amro-campaign-outcomes", className)}
      eyebrow="Campaign outcomes"
      title="Pipeline prepared"
      description={lastUpdated}
      status={<StatusIndicator label="Research complete" status="success" />}
      {...props}
    >
      <MetricGrid
        items={[
          {
            id: "businesses",
            label: "Businesses found",
            value: businesses,
            detail: "Matched to ICP"
          },
          { id: "people", label: "People identified", value: people, detail: "Decision makers" },
          {
            id: "messages",
            label: "Messages prepared",
            value: messages,
            detail: "Quality checked"
          },
          {
            id: "approved",
            label: "Approved to send",
            value: approved,
            detail: `${messages - approved} awaiting review`
          }
        ]}
      />
    </FeaturePanel>
  );
}

export type CampaignStatus = "draft" | "researching" | "review" | "approved" | "complete";
export interface CampaignStatusTabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  value?: CampaignStatus;
  counts?: Partial<Record<CampaignStatus, number>>;
  onChange?: (status: CampaignStatus) => void;
}
const campaignStatuses: readonly { id: CampaignStatus; label: string }[] = [
  { id: "draft", label: "Draft" },
  { id: "researching", label: "Researching" },
  { id: "review", label: "Ready for review" },
  { id: "approved", label: "Approved" },
  { id: "complete", label: "Complete" }
];
export function CampaignStatusTabs({
  className,
  counts = { review: 12, approved: 18 },
  onChange,
  value: controlled,
  ...props
}: CampaignStatusTabsProps) {
  const [internal, setInternal] = useState<CampaignStatus>("review");
  const value = controlled ?? internal;
  return (
    <div
      className={cn("amro-campaign-tabs", className)}
      role="tablist"
      aria-label="Campaign status"
      {...props}
    >
      {campaignStatuses.map((status) => (
        <button
          aria-selected={value === status.id}
          key={status.id}
          onClick={() => {
            setInternal(status.id);
            onChange?.(status.id);
          }}
          role="tab"
          type="button"
        >
          <span>{status.label}</span>
          {counts[status.id] !== undefined && <Badge tone="neutral">{counts[status.id]}</Badge>}
        </button>
      ))}
    </div>
  );
}

export type SalesChannel = "email" | "linkedin" | "sms" | "whatsapp";
export interface ChannelState {
  id: SalesChannel;
  label: ReactNode;
  status: "ready" | "needs-setup" | "unavailable";
  detail?: ReactNode;
}
export interface ChannelReadinessStripProps extends HTMLAttributes<HTMLDivElement> {
  channels?: readonly ChannelState[];
  onConfigure?: (channel: SalesChannel) => void;
}
const defaultChannels: readonly ChannelState[] = [
  { id: "email", label: "Email", status: "ready", detail: "SPF and DKIM verified" },
  { id: "linkedin", label: "LinkedIn", status: "needs-setup", detail: "Connect an account" },
  { id: "sms", label: "SMS", status: "unavailable", detail: "Not enabled" },
  { id: "whatsapp", label: "WhatsApp", status: "ready", detail: "Business profile connected" }
];
export function ChannelReadinessStrip({
  channels = defaultChannels,
  className,
  onConfigure,
  ...props
}: ChannelReadinessStripProps) {
  return (
    <div className={cn("amro-channel-strip", className)} {...props}>
      {channels.map((channel) => (
        <button key={channel.id} onClick={() => onConfigure?.(channel.id)} type="button">
          <StatusIndicator
            label={channel.label}
            status={
              channel.status === "ready"
                ? "success"
                : channel.status === "needs-setup"
                  ? "warning"
                  : "offline"
            }
          />
          <small>{channel.detail}</small>
        </button>
      ))}
    </div>
  );
}

export interface PersonalisedMessagePreviewProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "onChange"
> {
  recipient?: DecisionMaker;
  subject?: string;
  message?: string;
  editable?: boolean;
  onChange?: (message: string) => void;
  onApprove?: () => void;
}
export function PersonalisedMessagePreview({
  className,
  editable = false,
  message = "Maya, I noticed Northstar is hiring across customer success. Teams at that stage often need a faster way to resolve repetitive support work without losing the human relationship.",
  onApprove,
  onChange,
  recipient = samplePerson,
  subject = "Scaling support without adding queues",
  ...props
}: PersonalisedMessagePreviewProps) {
  return (
    <FeaturePanel
      className={cn("amro-message-preview", className)}
      eyebrow="Personalised outreach"
      title={subject}
      description={
        <span>
          To {recipient.name} · {recipient.title}
        </span>
      }
      status={<Badge tone="success">Quality checked</Badge>}
      {...props}
    >
      <aside>
        <Avatar
          name={recipient.name}
          {...(recipient.avatarUrl ? { src: recipient.avatarUrl } : {})}
        />
        <span>
          <strong>{recipient.name}</strong>
          <small>{recipient.company}</small>
        </span>
        <p>{recipient.relevance}</p>
      </aside>
      <div>
        {editable ? (
          <Textarea
            aria-label="Outreach message"
            onChange={(event) => onChange?.(event.target.value)}
            value={message}
          />
        ) : (
          <p>{message}</p>
        )}
        <small>
          <Sparkles aria-hidden="true" /> Grounded in 3 verified account facts
        </small>
      </div>
      {onApprove && <Button onClick={onApprove}>Approve message</Button>}
    </FeaturePanel>
  );
}

export interface SequenceStep {
  id: string;
  day: number;
  channel: SalesChannel;
  title: ReactNode;
  status?: "pending" | "ready" | "sent" | "blocked";
}
export interface SequenceTimelineProps extends HTMLAttributes<HTMLOListElement> {
  steps?: readonly SequenceStep[];
  onOpenStep?: (step: SequenceStep) => void;
}
const defaultSequence: readonly SequenceStep[] = [
  { id: "email-1", day: 1, channel: "email", title: "Personalised introduction", status: "ready" },
  { id: "linkedin", day: 3, channel: "linkedin", title: "LinkedIn follow-up", status: "blocked" },
  { id: "email-2", day: 6, channel: "email", title: "Share relevant proof", status: "pending" },
  { id: "email-3", day: 10, channel: "email", title: "Close the loop", status: "pending" }
];
export function SequenceTimeline({
  className,
  onOpenStep,
  steps = defaultSequence,
  ...props
}: SequenceTimelineProps) {
  return (
    <ol className={cn("amro-sequence", className)} {...props}>
      {steps.map((step) => (
        <li data-status={step.status ?? "pending"} key={step.id}>
          <span>Day {step.day}</span>
          <span>
            <Badge tone="neutral">{step.channel}</Badge>
            <strong>{step.title}</strong>
          </span>
          <StatusIndicator
            label={step.status ?? "pending"}
            status={
              step.status === "sent" || step.status === "ready"
                ? "success"
                : step.status === "blocked"
                  ? "blocked"
                  : "pending"
            }
          />
          {onOpenStep && (
            <button
              aria-label={`Open ${getAccessibleText(step.title, "sequence step")}`}
              onClick={() => onOpenStep(step)}
              type="button"
            >
              Open
            </button>
          )}
        </li>
      ))}
    </ol>
  );
}

export type MessageVariant = "concise" | "consultative" | "direct";
export interface MessageVariantSwitcherProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "onChange"
> {
  value?: MessageVariant;
  onChange?: (variant: MessageVariant) => void;
  previews?: Partial<Record<MessageVariant, ReactNode>>;
}
export function MessageVariantSwitcher({
  className,
  onChange,
  previews = {
    concise: "Short and focused",
    consultative: "Insight-led and exploratory",
    direct: "Clear ask and next step"
  },
  value: controlled,
  ...props
}: MessageVariantSwitcherProps) {
  const [internal, setInternal] = useState<MessageVariant>("consultative");
  const value = controlled ?? internal;
  return (
    <div className={cn("amro-message-variants", className)} {...props}>
      <div role="radiogroup" aria-label="Message tone">
        {(["concise", "consultative", "direct"] as const).map((variant) => (
          <button
            aria-checked={value === variant}
            key={variant}
            onClick={() => {
              setInternal(variant);
              onChange?.(variant);
            }}
            role="radio"
            type="button"
          >
            {variant}
          </button>
        ))}
      </div>
      <p>{previews[value]}</p>
    </div>
  );
}

export interface EvidenceSentence {
  id: string;
  sentence: ReactNode;
  facts: readonly string[];
}
export interface PersonalisationEvidenceProps extends HTMLAttributes<HTMLDivElement> {
  evidence?: readonly EvidenceSentence[];
}
export function PersonalisationEvidence({
  className,
  evidence = [
    {
      id: "one",
      sentence: "I noticed Northstar is hiring across customer success.",
      facts: ["3 open CS roles", "Careers page, today"]
    },
    {
      id: "two",
      sentence: "Teams at that stage often need to reduce repetitive work.",
      facts: ["Series B", "Support team grew 42%"]
    }
  ],
  ...props
}: PersonalisationEvidenceProps) {
  const [active, setActive] = useState(evidence[0]?.id);
  return (
    <FeaturePanel
      className={cn("amro-personalisation-evidence", className)}
      eyebrow="Personalisation evidence"
      title="Every sentence has a reason"
      {...props}
    >
      <div>
        {evidence.map((item) => (
          <button
            aria-pressed={active === item.id}
            key={item.id}
            onClick={() => setActive(item.id)}
            type="button"
          >
            {item.sentence}
          </button>
        ))}
      </div>
      <aside>
        {evidence
          .find((item) => item.id === active)
          ?.facts.map((fact) => (
            <span key={fact}>
              <Check aria-hidden="true" /> {fact}
            </span>
          ))}
      </aside>
    </FeaturePanel>
  );
}

export interface QualityDimension {
  id: string;
  label: ReactNode;
  score: number;
}
export interface OutreachQualityScoreProps extends HTMLAttributes<HTMLDivElement> {
  score?: number;
  dimensions?: readonly QualityDimension[];
}
const defaultQuality: readonly QualityDimension[] = [
  { id: "relevance", label: "Relevance", score: 94 },
  { id: "specificity", label: "Specificity", score: 88 },
  { id: "tone", label: "Tone", score: 91 },
  { id: "clarity", label: "Clarity", score: 96 },
  { id: "risk", label: "Low risk", score: 90 }
];
export function OutreachQualityScore({
  className,
  dimensions = defaultQuality,
  score = 92,
  ...props
}: OutreachQualityScoreProps) {
  return (
    <Card className={cn("amro-outreach-quality", className)} {...props}>
      <header>
        <span>
          <small>Outreach quality</small>
          <strong>
            {score}
            <i>/100</i>
          </strong>
        </span>
        <StatusIndicator
          label={score >= 85 ? "Ready for review" : "Needs revision"}
          status={score >= 85 ? "success" : "warning"}
        />
      </header>
      <ul>
        {dimensions.map((item) => (
          <li key={item.id}>
            <span>{item.label}</span>
            <meter max="100" value={item.score}>
              {item.score}%
            </meter>
            <strong>{item.score}</strong>
          </li>
        ))}
      </ul>
    </Card>
  );
}

export interface WeakDraftWarningProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  reasons?: readonly ReactNode[];
  onFix?: () => void;
  onDismiss?: () => void;
}
export function WeakDraftWarning({
  className,
  onDismiss,
  onFix,
  reasons = [
    "Opening line could apply to any company",
    "Call to action asks for too much commitment"
  ],
  title = "Quality review rejected this draft",
  ...props
}: WeakDraftWarningProps) {
  return (
    <div className={cn("amro-weak-draft", className)} role="alert" {...props}>
      <CircleAlert aria-hidden="true" />
      <span>
        <strong>{title}</strong>
        <ul>
          {reasons.map((reason, index) => (
            <li key={index}>{reason}</li>
          ))}
        </ul>
      </span>
      <div>
        {onDismiss && (
          <Button onClick={onDismiss} variant="tertiary">
            Dismiss
          </Button>
        )}
        {onFix && <Button onClick={onFix}>Fix weak sections</Button>}
      </div>
    </div>
  );
}

export interface RetryWithGuidanceProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
  section?: ReactNode;
  defaultGuidance?: string;
  onSubmit?: (guidance: string) => void;
}
export function RetryWithGuidance({
  className,
  defaultGuidance = "Use the hiring signal in the opening and make the call to action lower commitment.",
  onSubmit,
  section = "Opening and call to action",
  ...props
}: RetryWithGuidanceProps) {
  const [guidance, setGuidance] = useState(defaultGuidance);
  return (
    <form
      className={cn("amro-retry-guidance", className)}
      onSubmit={(event) => {
        event.preventDefault();
        onSubmit?.(guidance);
      }}
      {...props}
    >
      <label>
        <span>Regenerate {section}</span>
        <Textarea onChange={(event) => setGuidance(event.target.value)} value={guidance} />
      </label>
      <div>
        <small>Only this portion changes. Approved copy is preserved.</small>
        <Button disabled={!guidance.trim()} type="submit">
          <Sparkles aria-hidden="true" /> Regenerate section
        </Button>
      </div>
    </form>
  );
}

export interface ApprovalGateProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  consequence?: ReactNode;
  dirty?: boolean;
  onEdit?: () => void;
  onApprove?: () => void;
  onReject?: () => void;
}
export function ApprovalGate({
  className,
  consequence = "Approving schedules 18 emails inside the configured send window.",
  dirty = false,
  onApprove,
  onEdit,
  onReject,
  title = "Review before anything is sent",
  ...props
}: ApprovalGateProps) {
  return (
    <FeaturePanel
      className={cn("amro-approval-gate", className)}
      eyebrow="Human control boundary"
      title={title}
      description={consequence}
      status={
        <StatusIndicator
          label={dirty ? "Edited" : "Awaiting review"}
          status={dirty ? "warning" : "pending"}
        />
      }
      {...props}
    >
      <div>
        <ShieldCheck aria-hidden="true" />
        <span>AmroGen is paused. No external action has occurred.</span>
      </div>
      <footer>
        {onReject && (
          <Button onClick={onReject} variant="tertiary">
            Reject
          </Button>
        )}
        {onEdit && (
          <Button onClick={onEdit} variant="secondary">
            Edit draft
          </Button>
        )}
        {onApprove && <Button onClick={onApprove}>Approve and schedule</Button>}
      </footer>
    </FeaturePanel>
  );
}

export interface ReviewQueueItem {
  id: string;
  recipient: string;
  company: string;
  score: number;
  status?: "pending" | "approved" | "rejected";
}
export interface BulkReviewQueueProps extends HTMLAttributes<HTMLDivElement> {
  items?: readonly ReviewQueueItem[];
  onDecision?: (item: ReviewQueueItem, decision: "approve" | "reject" | "skip") => void;
}
const reviewItems: readonly ReviewQueueItem[] = [
  { id: "maya", recipient: "Maya Chen", company: "Northstar Labs", score: 92 },
  { id: "sam", recipient: "Sam Wilson", company: "Atlas CX", score: 87 },
  { id: "lee", recipient: "Lee Morgan", company: "Orbit Desk", score: 83 }
];
export function BulkReviewQueue({
  className,
  items = reviewItems,
  onDecision,
  ...props
}: BulkReviewQueueProps) {
  const [index, setIndex] = useState(0);
  const item = items[index];
  if (!item)
    return (
      <EmptyNotice
        className={className}
        title="Review queue complete"
        description="All messages have a decision."
      />
    );
  const decide = (decision: "approve" | "reject" | "skip") => {
    onDecision?.(item, decision);
    setIndex((value) => value + 1);
  };
  return (
    <FeaturePanel
      className={cn("amro-bulk-review", className)}
      eyebrow={`Review ${index + 1} of ${items.length}`}
      title={item.recipient}
      description={item.company}
      status={<Badge tone="neutral">{item.score}/100 quality</Badge>}
      {...props}
    >
      <p>Personalised message ready for human review.</p>
      <div>
        <Button onClick={() => decide("reject")} variant="tertiary">
          R · Reject
        </Button>
        <Button onClick={() => decide("skip")} variant="secondary">
          S · Skip
        </Button>
        <Button onClick={() => decide("approve")}>A · Approve</Button>
      </div>
    </FeaturePanel>
  );
}

export interface ApprovalDiffProps extends HTMLAttributes<HTMLDivElement> {
  original?: string;
  revised?: string;
  editor?: ReactNode;
}
export function ApprovalDiff({
  className,
  editor = "Maya",
  original = "Would you have 30 minutes to discuss our platform?",
  revised = "Worth a 15-minute look next Tuesday?",
  ...props
}: ApprovalDiffProps) {
  return (
    <FeaturePanel
      className={cn("amro-approval-diff", className)}
      eyebrow="Human edit"
      title="What changed"
      description={`Edited by ${getAccessibleText(editor, "human reviewer")}`}
      {...props}
    >
      <div>
        <section>
          <Badge tone="neutral">AI draft</Badge>
          <del>{original}</del>
        </section>
        <section>
          <Badge tone="success">Approved edit</Badge>
          <ins>{revised}</ins>
        </section>
      </div>
    </FeaturePanel>
  );
}

export interface SendWindow {
  timezone: string;
  weekdays: readonly string[];
  start: string;
  end: string;
  dailyCap: number;
  excludeHolidays: boolean;
}
export interface SendWindowPlannerProps extends Omit<HTMLAttributes<HTMLFormElement>, "onChange"> {
  value?: SendWindow;
  onChange?: (window: SendWindow) => void;
}
export function SendWindowPlanner({
  className,
  onChange,
  value = {
    timezone: "Europe/London",
    weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri"],
    start: "09:00",
    end: "16:30",
    dailyCap: 40,
    excludeHolidays: true
  },
  ...props
}: SendWindowPlannerProps) {
  const [window, setWindow] = useState(value);
  const update = <K extends keyof SendWindow>(key: K, next: SendWindow[K]) => {
    const updated = { ...window, [key]: next };
    setWindow(updated);
    onChange?.(updated);
  };
  return (
    <form className={cn("amro-send-window", className)} {...props}>
      <header>
        <span>
          <strong>Send window</strong>
          <small>Recipient-local delivery controls</small>
        </span>
        <Badge tone="success">Safe sending on</Badge>
      </header>
      <Input
        label="Timezone"
        value={window.timezone}
        onChange={(event) => update("timezone", event.target.value)}
      />
      <div className="amro-send-window__days">
        {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => (
          <button
            aria-pressed={window.weekdays.includes(day)}
            key={day}
            onClick={() =>
              update(
                "weekdays",
                window.weekdays.includes(day)
                  ? window.weekdays.filter((item) => item !== day)
                  : [...window.weekdays, day]
              )
            }
            type="button"
          >
            {day}
          </button>
        ))}
      </div>
      <div>
        <label className="amro-field">
          <span className="amro-field__label">Start</span>
          <TimeInput
            value={window.start}
            onChange={(event) => update("start", event.target.value)}
          />
        </label>
        <label className="amro-field">
          <span className="amro-field__label">End</span>
          <TimeInput value={window.end} onChange={(event) => update("end", event.target.value)} />
        </label>
        <Input
          label="Daily cap"
          min="1"
          type="number"
          value={window.dailyCap}
          onChange={(event) => update("dailyCap", Number(event.target.value))}
        />
      </div>
      <label>
        <input
          checked={window.excludeHolidays}
          onChange={(event) => update("excludeHolidays", event.target.checked)}
          type="checkbox"
        />{" "}
        Exclude public holidays
      </label>
    </form>
  );
}

export interface CampaignProgressCardProps extends HTMLAttributes<HTMLDivElement> {
  stage?: ReactNode;
  progress?: number;
  blockers?: readonly ReactNode[];
  nextAction?: ReactNode;
  onContinue?: () => void;
}
export function CampaignProgressCard({
  blockers = ["Connect LinkedIn to enable step 2"],
  className,
  nextAction = "Review 12 prepared messages",
  onContinue,
  progress = 72,
  stage = "Ready for review",
  ...props
}: CampaignProgressCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-campaign-progress", className)}
      eyebrow="Campaign progress"
      title={stage}
      status={<strong>{progress}%</strong>}
      {...props}
    >
      <progress max="100" value={progress}>
        {progress}%
      </progress>
      {blockers.length > 0 && (
        <ul>
          {blockers.map((blocker, index) => (
            <li key={index}>
              <CircleAlert aria-hidden="true" /> {blocker}
            </li>
          ))}
        </ul>
      )}
      <footer>
        <span>
          <small>Next action</small>
          <strong>{nextAction}</strong>
        </span>
        {onContinue && <Button onClick={onContinue}>Open review queue</Button>}
      </footer>
    </FeaturePanel>
  );
}

export interface CampaignCommandBarProps extends HTMLAttributes<HTMLDivElement> {
  state?: "running" | "paused" | "complete";
  reviewCount?: number;
  onPause?: () => void;
  onResume?: () => void;
  onDuplicate?: () => void;
  onArchive?: () => void;
  onOpenReview?: () => void;
}
export function CampaignCommandBar({
  className,
  onArchive,
  onDuplicate,
  onOpenReview,
  onPause,
  onResume,
  reviewCount = 12,
  state = "running",
  ...props
}: CampaignCommandBarProps) {
  return (
    <div className={cn("amro-campaign-command", className)} {...props}>
      <StatusIndicator
        label={state}
        status={state === "running" ? "active" : state === "complete" ? "complete" : "idle"}
      />
      <div>
        {state === "running" && onPause && (
          <Button onClick={onPause} variant="secondary">
            Pause
          </Button>
        )}
        {state === "paused" && onResume && <Button onClick={onResume}>Resume</Button>}
        {onOpenReview && (
          <Button onClick={onOpenReview}>
            Review queue <Badge tone="neutral">{reviewCount}</Badge>
          </Button>
        )}
        {onDuplicate && (
          <Button onClick={onDuplicate} variant="tertiary">
            Duplicate
          </Button>
        )}
        {onArchive && (
          <Button onClick={onArchive} variant="tertiary">
            Archive
          </Button>
        )}
      </div>
    </div>
  );
}

export interface CreditUsageCardProps extends HTMLAttributes<HTMLDivElement> {
  predicted?: number;
  actual?: number;
  budget?: number;
  breakdown?: readonly { id: string; label: ReactNode; credits: number }[];
}
export function CreditUsageCard({
  actual = 640,
  breakdown = [
    { id: "research", label: "Research", credits: 280 },
    { id: "enrich", label: "Enrichment", credits: 210 },
    { id: "messages", label: "Messaging", credits: 150 }
  ],
  budget = 1200,
  className,
  predicted = 920,
  ...props
}: CreditUsageCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-credit-usage", className)}
      eyebrow="Credit usage"
      title={`${actual.toLocaleString()} used`}
      description={`${predicted.toLocaleString()} predicted · ${budget.toLocaleString()} budget`}
      status={
        <Badge tone={predicted <= budget ? "success" : "info"}>
          {predicted <= budget ? "Within budget" : "Over budget"}
        </Badge>
      }
      {...props}
    >
      <progress max={budget} value={actual}>
        {actual} of {budget}
      </progress>
      <ul>
        {breakdown.map((item) => (
          <li key={item.id}>
            <span>{item.label}</span>
            <strong>{item.credits}</strong>
          </li>
        ))}
      </ul>
    </FeaturePanel>
  );
}

export interface SenderIdentityCardProps extends HTMLAttributes<HTMLDivElement> {
  name?: ReactNode;
  email?: ReactNode;
  provider?: ReactNode;
  health?: number;
  authentication?: readonly { label: ReactNode; valid: boolean }[];
  onManage?: () => void;
}
export function SenderIdentityCard({
  authentication = [
    { label: "SPF", valid: true },
    { label: "DKIM", valid: true },
    { label: "DMARC", valid: true }
  ],
  className,
  email = "maya@northstar.example",
  health = 96,
  name = "Maya Chen",
  onManage,
  provider = "Google Workspace",
  ...props
}: SenderIdentityCardProps) {
  return (
    <Card className={cn("amro-sender-identity", className)} {...props}>
      <header>
        <Avatar name={getAccessibleText(name, "Sender")} />
        <span>
          <strong>{name}</strong>
          <small>
            {email} · {provider}
          </small>
        </span>
        <StatusIndicator
          label={`${health}% mailbox health`}
          status={health >= 85 ? "success" : "warning"}
        />
      </header>
      <div>
        {authentication.map((item, index) => (
          <Badge
            key={getAccessibleText(item.label, `authentication-${index}`)}
            tone={item.valid ? "success" : "info"}
          >
            {item.valid ? <Check aria-hidden="true" /> : <CircleAlert aria-hidden="true" />}
            {item.label}
          </Badge>
        ))}
      </div>
      {onManage && (
        <Button onClick={onManage} variant="secondary">
          Manage sender
        </Button>
      )}
    </Card>
  );
}

export type AutonomyLevel = "research" | "drafts" | "approval" | "automation";
export interface AutonomyLevelControlProps extends Omit<
  HTMLAttributes<HTMLFieldSetElement>,
  "onChange"
> {
  value?: AutonomyLevel;
  onChange?: (level: AutonomyLevel) => void;
  automationApproved?: boolean;
}
const autonomyLevels: readonly { id: AutonomyLevel; label: string; detail: string }[] = [
  { id: "research", label: "Research only", detail: "Find accounts and people" },
  { id: "drafts", label: "Prepare drafts", detail: "Write messages but do not send" },
  { id: "approval", label: "Require approval", detail: "A human approves every send" },
  { id: "automation", label: "Approved automation", detail: "Send only inside pre-approved rules" }
];
export function AutonomyLevelControl({
  automationApproved = false,
  className,
  onChange,
  value: controlled,
  ...props
}: AutonomyLevelControlProps) {
  const [internal, setInternal] = useState<AutonomyLevel>("approval");
  const value = controlled ?? internal;
  return (
    <fieldset className={cn("amro-autonomy", className)} {...props}>
      <legend>Agent autonomy</legend>
      {autonomyLevels.map((level) => {
        const disabled = level.id === "automation" && !automationApproved;
        return (
          <label key={level.id} data-selected={value === level.id}>
            <input
              checked={value === level.id}
              disabled={disabled}
              name="autonomy"
              onChange={() => {
                setInternal(level.id);
                onChange?.(level.id);
              }}
              type="radio"
            />
            <span>
              <strong>{level.label}</strong>
              <small>
                {level.detail}
                {disabled ? " · Admin approval required" : ""}
              </small>
            </span>
          </label>
        );
      })}
    </fieldset>
  );
}

export interface SalesAuditEntry {
  id: string;
  time: ReactNode;
  actor: ReactNode;
  action: ReactNode;
  target: ReactNode;
  outcome: "success" | "pending" | "blocked" | "failed";
}
export interface SalesAgentAuditLogProps extends HTMLAttributes<HTMLDivElement> {
  entries?: readonly SalesAuditEntry[];
  onSearch?: (query: string) => void;
  onOpen?: (entry: SalesAuditEntry) => void;
}
const salesAudit: readonly SalesAuditEntry[] = [
  {
    id: "one",
    time: "10:42",
    actor: "Research agent",
    action: "Enriched",
    target: "Northstar Labs",
    outcome: "success"
  },
  {
    id: "two",
    time: "10:45",
    actor: "Maya Chen",
    action: "Edited message",
    target: "Opening sentence",
    outcome: "success"
  },
  {
    id: "three",
    time: "10:47",
    actor: "AmroGen",
    action: "Requested approval",
    target: "18 messages",
    outcome: "pending"
  }
];
export function SalesAgentAuditLog({
  className,
  entries = salesAudit,
  onOpen,
  onSearch,
  ...props
}: SalesAgentAuditLogProps) {
  const [query, setQuery] = useState("");
  const filtered = useMemo(
    () =>
      entries.filter((entry) =>
        `${getAccessibleText(entry.actor, "")} ${getAccessibleText(entry.action, "")} ${getAccessibleText(entry.target, "")}`
          .toLowerCase()
          .includes(query.toLowerCase())
      ),
    [entries, query]
  );
  return (
    <div className={cn("amro-sales-audit", className)} {...props}>
      <header>
        <span>
          <strong>Sales agent audit log</strong>
          <small>{entries.length} governed events</small>
        </span>
        <label>
          <Search aria-hidden="true" />
          <input
            aria-label="Search audit log"
            onChange={(event) => {
              setQuery(event.target.value);
              onSearch?.(event.target.value);
            }}
            placeholder="Search actions…"
            value={query}
          />
        </label>
      </header>
      <div role="table" aria-label="Sales agent audit events">
        {filtered.map((entry) => (
          <button key={entry.id} onClick={() => onOpen?.(entry)} role="row" type="button">
            <span role="cell">{entry.time}</span>
            <strong role="cell">{entry.actor}</strong>
            <span role="cell">{entry.action}</span>
            <span role="cell">{entry.target}</span>
            <StatusIndicator
              label={entry.outcome}
              status={entry.outcome === "failed" ? "error" : entry.outcome}
            />
          </button>
        ))}
      </div>
      {filtered.length === 0 && <EmptyNotice title="No matching audit events" />}
    </div>
  );
}
```



## Usage

Shared product source installed automatically by AmroUI component entries.

