# AmroUI editorial

Installable AmroUI product component source.

## Installation

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

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

## Source

### source/components/editorial.tsx

```tsx
import {
  AlertTriangle,
  Accessibility,
  ArrowDown,
  ArrowUp,
  BookOpenCheck,
  Check,
  CheckCircle2,
  ChevronRight,
  CircleAlert,
  Clock3,
  Code2,
  Download,
  Eye,
  FileText,
  Globe2,
  GripVertical,
  Image as ImageIcon,
  Link2,
  LockKeyhole,
  MapPin,
  MessageSquare,
  Plus,
  Quote,
  RefreshCw,
  Search,
  Sparkles,
  Target,
  Upload,
  UserRound,
  Webhook,
  X
} from "lucide-react";
import { useState, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "../lib/cn.js";
import { Badge } from "./badge.js";
import { Button } from "./button.js";
import { Card } from "./card.js";
import { EmptyNotice, FeaturePanel, StatusIndicator } from "./feature-primitives.js";
import { Input, Textarea } from "./input.js";

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

export type EditorialStageStatus = "pending" | "active" | "complete" | "blocked" | "error";
export interface PipelineStageCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  title?: ReactNode;
  description?: ReactNode;
  status?: EditorialStageStatus;
  owner?: ReactNode;
  elapsed?: ReactNode;
  warnings?: readonly ReactNode[];
  nextAction?: ReactNode;
  onContinue?: () => void;
}

export function PipelineStageCard({
  className,
  description = "Analyse ranking pages, recurring themes, and evidence gaps.",
  elapsed = "02:18",
  nextAction = "Review research",
  onContinue,
  owner = "Research agent",
  status = "active",
  title = "SERP research",
  warnings = [],
  ...props
}: PipelineStageCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-pipeline-stage-card", className)}
      eyebrow="Editorial stage"
      title={title}
      description={description}
      status={<StatusIndicator label={status} status={status} />}
      {...props}
    >
      <div className="amro-pipeline-stage-card__meta">
        <span>
          <UserRound aria-hidden="true" />
          <small>Owner</small>
          <strong>{owner}</strong>
        </span>
        <span>
          <Clock3 aria-hidden="true" />
          <small>Elapsed</small>
          <strong>{elapsed}</strong>
        </span>
      </div>
      {warnings.length > 0 && (
        <ul>
          {warnings.map((warning, index) => (
            <li key={index}>
              <AlertTriangle aria-hidden="true" />
              {warning}
            </li>
          ))}
        </ul>
      )}
      <footer>
        <span>
          <small>Next action</small>
          <strong>{nextAction}</strong>
        </span>
        {onContinue && (
          <Button onClick={onContinue}>
            Continue
            <ChevronRight aria-hidden="true" />
          </Button>
        )}
      </footer>
    </FeaturePanel>
  );
}

export type SearchIntent = "informational" | "commercial" | "transactional" | "mixed";
export interface KeywordBrief {
  keyword: string;
  domain: string;
  audience: string;
  location: string;
  intent: SearchIntent;
}
export interface KeywordBriefFormProps extends Omit<
  HTMLAttributes<HTMLFormElement>,
  "onSubmit" | "defaultValue"
> {
  defaultValue?: Partial<KeywordBrief>;
  onSubmit?: (brief: KeywordBrief) => void;
}

export function KeywordBriefForm({
  className,
  defaultValue,
  onSubmit,
  ...props
}: KeywordBriefFormProps) {
  const [brief, setBrief] = useState<KeywordBrief>({
    keyword: defaultValue?.keyword ?? "AI customer support automation",
    domain: defaultValue?.domain ?? "amroagents.com",
    audience: defaultValue?.audience ?? "Customer experience leaders",
    location: defaultValue?.location ?? "United Kingdom",
    intent: defaultValue?.intent ?? "commercial"
  });
  const update = <K extends keyof KeywordBrief>(key: K, value: KeywordBrief[K]) =>
    setBrief((current) => ({ ...current, [key]: value }));
  const ready =
    brief.keyword.trim().length > 1 &&
    brief.domain.trim().length > 1 &&
    brief.audience.trim().length > 1;
  return (
    <form
      className={cn("amro-keyword-brief", className)}
      onSubmit={(event) => {
        event.preventDefault();
        if (ready) onSubmit?.(brief);
      }}
      {...props}
    >
      <FeaturePanel
        eyebrow="Content brief"
        title="Define the search opportunity"
        description="Give the research agent enough context to avoid generic output."
      >
        <div className="amro-keyword-brief__grid">
          <Input
            label="Primary keyword"
            value={brief.keyword}
            onChange={(event) => update("keyword", event.target.value)}
          />
          <Input
            label="Publishing domain"
            value={brief.domain}
            onChange={(event) => update("domain", event.target.value)}
          />
          <Input
            label="Audience"
            value={brief.audience}
            onChange={(event) => update("audience", event.target.value)}
          />
          <Input
            label="Search location"
            value={brief.location}
            onChange={(event) => update("location", event.target.value)}
          />
          <SearchIntentSelector
            value={brief.intent}
            onChange={(intent) => update("intent", intent)}
          />
        </div>
        <footer>
          <span>
            <Globe2 aria-hidden="true" /> Research respects the selected market and domain.
          </span>
          <Button disabled={!ready} type="submit">
            Start research
            <Search aria-hidden="true" />
          </Button>
        </footer>
      </FeaturePanel>
    </form>
  );
}

export interface SERPPage {
  id: string;
  title: ReactNode;
  url: string;
  position: number;
  authority?: number;
  themes?: readonly string[];
  gaps?: readonly string[];
  selected?: boolean;
}
export interface SERPResearchPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  pages?: readonly SERPPage[];
  query?: ReactNode;
  recurringThemes?: readonly string[];
  evidenceGaps?: readonly string[];
  onSelect?: (page: SERPPage) => void;
}
const sampleSerpPages: readonly SERPPage[] = [
  {
    id: "one",
    title: "AI support automation guide",
    url: "example.com/guide",
    position: 1,
    authority: 82,
    themes: ["routing", "knowledge"],
    gaps: ["Human approval"]
  },
  {
    id: "two",
    title: "Customer support AI platforms",
    url: "example.org/platforms",
    position: 2,
    authority: 76,
    themes: ["ROI", "integrations"],
    gaps: ["Agent governance"]
  },
  {
    id: "three",
    title: "Automating customer operations",
    url: "example.net/operations",
    position: 3,
    authority: 71,
    themes: ["workflows"],
    gaps: ["Verified outcomes"]
  }
];
export function SERPResearchPanel({
  className,
  evidenceGaps = ["Human approval boundaries", "Source verification", "Real outcome metrics"],
  onSelect,
  pages = sampleSerpPages,
  query = "AI customer support automation",
  recurringThemes = ["Ticket routing", "Knowledge bases", "Response time", "Integrations"],
  ...props
}: SERPResearchPanelProps) {
  const [tab, setTab] = useState<"pages" | "themes" | "gaps">("pages");
  return (
    <FeaturePanel
      className={cn("amro-serp-research", className)}
      eyebrow="Live SERP research"
      title={query}
      description={`${pages.length} competing pages analysed`}
      status={<StatusIndicator label="Evidence ready" status="success" />}
      {...props}
    >
      <div className="amro-serp-research__tabs" role="tablist">
        {(["pages", "themes", "gaps"] as const).map((item) => (
          <button
            aria-selected={tab === item}
            key={item}
            onClick={() => setTab(item)}
            role="tab"
            type="button"
          >
            {item}
          </button>
        ))}
      </div>
      {tab === "pages" && (
        <ol>
          {pages.map((page) => (
            <li key={page.id}>
              <span>{page.position}</span>
              <button onClick={() => onSelect?.(page)} type="button">
                <strong>{page.title}</strong>
                <small>{page.url}</small>
              </button>
              <Badge tone="neutral">DA {page.authority ?? "—"}</Badge>
            </li>
          ))}
        </ol>
      )}
      {tab === "themes" && (
        <div className="amro-serp-research__chips">
          {recurringThemes.map((theme) => (
            <Badge key={theme} tone="info">
              {theme}
            </Badge>
          ))}
        </div>
      )}
      {tab === "gaps" && (
        <ul className="amro-serp-research__gaps">
          {evidenceGaps.map((gap) => (
            <li key={gap}>
              <Target aria-hidden="true" />
              <span>
                <strong>{gap}</strong>
                <small>Weak or missing across current results</small>
              </span>
            </li>
          ))}
        </ul>
      )}
    </FeaturePanel>
  );
}

export interface KeywordCluster {
  id: string;
  label: string;
  volume?: number;
  difficulty?: number;
  kind: "primary" | "semantic" | "question";
}
export interface KeywordClusterMapProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  keyword?: string;
  clusters?: readonly KeywordCluster[];
  onSelect?: (cluster: KeywordCluster) => void;
}
const sampleClusters: readonly KeywordCluster[] = [
  { id: "primary", label: "AI support automation", volume: 2400, difficulty: 61, kind: "primary" },
  {
    id: "semantic-1",
    label: "customer service AI",
    volume: 1800,
    difficulty: 58,
    kind: "semantic"
  },
  {
    id: "semantic-2",
    label: "automated support workflows",
    volume: 720,
    difficulty: 42,
    kind: "semantic"
  },
  {
    id: "question-1",
    label: "How does AI automate support?",
    volume: 390,
    difficulty: 31,
    kind: "question"
  },
  {
    id: "question-2",
    label: "Is customer support AI safe?",
    volume: 210,
    difficulty: 25,
    kind: "question"
  }
];
export function KeywordClusterMap({
  className,
  clusters = sampleClusters,
  keyword = "AI support automation",
  onSelect,
  ...props
}: KeywordClusterMapProps) {
  return (
    <div className={cn("amro-keyword-cluster", className)} {...props}>
      <header>
        <span>
          <strong>{keyword}</strong>
          <small>Semantic topic model</small>
        </span>
        <Badge tone="success">{clusters.length} opportunities</Badge>
      </header>
      <div>
        {clusters.map((cluster) => (
          <button
            data-kind={cluster.kind}
            key={cluster.id}
            onClick={() => onSelect?.(cluster)}
            type="button"
          >
            <strong>{cluster.label}</strong>
            <small>
              {cluster.volume?.toLocaleString()} searches · KD {cluster.difficulty}
            </small>
          </button>
        ))}
      </div>
      <footer>
        <span>
          <i data-kind="primary" />
          Primary
        </span>
        <span>
          <i data-kind="semantic" />
          Semantic
        </span>
        <span>
          <i data-kind="question" />
          Question
        </span>
      </footer>
    </div>
  );
}

export interface SearchIntentSelectorProps extends Omit<
  HTMLAttributes<HTMLFieldSetElement>,
  "onChange"
> {
  value?: SearchIntent;
  onChange?: (intent: SearchIntent) => void;
}
const intentDetails: Record<SearchIntent, string> = {
  informational: "Learn or understand",
  commercial: "Compare possible solutions",
  transactional: "Take an immediate action",
  mixed: "Several intents share the results"
};
export function SearchIntentSelector({
  className,
  onChange,
  value: controlled,
  ...props
}: SearchIntentSelectorProps) {
  const [internal, setInternal] = useState<SearchIntent>("commercial");
  const value = controlled ?? internal;
  return (
    <fieldset className={cn("amro-search-intent", className)} {...props}>
      <legend>Search intent</legend>
      <div>
        {(Object.keys(intentDetails) as SearchIntent[]).map((intent) => (
          <label data-selected={value === intent} key={intent}>
            <input
              checked={value === intent}
              name="search-intent"
              onChange={() => {
                setInternal(intent);
                onChange?.(intent);
              }}
              type="radio"
            />
            <span>
              <strong>{intent}</strong>
              <small>{intentDetails[intent]}</small>
            </span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

export interface BriefSection {
  id: string;
  heading: string;
  questions: readonly string[];
  references: readonly string[];
  wordTarget: number;
  instructions?: string;
}
export interface ContentBriefCanvasProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  sections?: readonly BriefSection[];
  onChange?: (sections: readonly BriefSection[]) => void;
  onGenerateOutline?: (sections: readonly BriefSection[]) => void;
}
const sampleBriefSections: readonly BriefSection[] = [
  {
    id: "intro",
    heading: "What AI support automation means",
    questions: ["Which tasks are safe to automate?"],
    references: ["Industry benchmark report"],
    wordTarget: 350,
    instructions: "Define terms without hype."
  },
  {
    id: "workflow",
    heading: "How governed agents work",
    questions: ["Where does human approval belong?"],
    references: ["AmroAgents control model"],
    wordTarget: 700,
    instructions: "Use an operational example."
  }
];
export function ContentBriefCanvas({
  className,
  onChange,
  onGenerateOutline,
  sections = sampleBriefSections,
  ...props
}: ContentBriefCanvasProps) {
  const [items, setItems] = useState([...sections]);
  const total = items.reduce((sum, item) => sum + item.wordTarget, 0);
  const update = (id: string, patch: Partial<BriefSection>) => {
    const next = items.map((item) => (item.id === id ? { ...item, ...patch } : item));
    setItems(next);
    onChange?.(next);
  };
  return (
    <FeaturePanel
      className={cn("amro-content-brief", className)}
      eyebrow="Content brief"
      title="Article plan"
      description={`${items.length} sections · ${total.toLocaleString()} target words`}
      actions={
        <Button onClick={() => onGenerateOutline?.(items)} size="sm">
          Generate outline
          <Sparkles aria-hidden="true" />
        </Button>
      }
      {...props}
    >
      <div>
        {items.map((section, index) => (
          <article key={section.id}>
            <header>
              <span>{index + 1}</span>
              <Input
                aria-label={`Heading ${index + 1}`}
                value={section.heading}
                onChange={(event) => update(section.id, { heading: event.target.value })}
              />
              <label>
                <input
                  onChange={(event) =>
                    update(section.id, { wordTarget: Number(event.target.value) })
                  }
                  type="number"
                  value={section.wordTarget}
                />
                <span>words</span>
              </label>
            </header>
            <div>
              <span>
                <strong>Questions</strong>
                {section.questions.map((question) => (
                  <Badge key={question} tone="neutral">
                    {question}
                  </Badge>
                ))}
              </span>
              <span>
                <strong>References</strong>
                {section.references.map((reference) => (
                  <Badge key={reference} tone="info">
                    <Link2 aria-hidden="true" />
                    {reference}
                  </Badge>
                ))}
              </span>
            </div>
            <Textarea
              aria-label={`Instructions for ${section.heading}`}
              onChange={(event) => update(section.id, { instructions: event.target.value })}
              value={section.instructions ?? ""}
            />
          </article>
        ))}
      </div>
    </FeaturePanel>
  );
}

export interface OutlineNode {
  id: string;
  title: string;
  level: 2 | 3;
  suggested?: boolean;
  locked?: boolean;
}
export interface OutlineTreeEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  nodes?: readonly OutlineNode[];
  onChange?: (nodes: readonly OutlineNode[]) => void;
  onAcceptSuggestion?: (node: OutlineNode) => void;
}
const sampleOutline: readonly OutlineNode[] = [
  { id: "one", title: "What support automation can safely handle", level: 2 },
  { id: "two", title: "Triage and knowledge retrieval", level: 3 },
  { id: "three", title: "The human approval boundary", level: 2, suggested: true },
  { id: "four", title: "Measuring verified outcomes", level: 2 }
];
export function OutlineTreeEditor({
  className,
  nodes = sampleOutline,
  onAcceptSuggestion,
  onChange,
  ...props
}: OutlineTreeEditorProps) {
  const [items, setItems] = useState([...nodes]);
  const move = (index: number, offset: number) => {
    const target = index + offset;
    if (target < 0 || target >= items.length) return;
    const next = [...items];
    [next[index], next[target]] = [next[target]!, next[index]!];
    setItems(next);
    onChange?.(next);
  };
  const remove = (id: string) => {
    const next = items.filter((item) => item.id !== id);
    setItems(next);
    onChange?.(next);
  };
  return (
    <div className={cn("amro-outline-editor", className)} {...props}>
      <header>
        <span>
          <strong>Article outline</strong>
          <small>Drag or use arrow controls to reorder</small>
        </span>
        <Badge tone="neutral">{items.length} headings</Badge>
      </header>
      <ol>
        {items.map((node, index) => (
          <li data-level={node.level} draggable key={node.id}>
            <GripVertical aria-hidden="true" />
            <span>
              <Badge tone={node.suggested ? "info" : "neutral"}>H{node.level}</Badge>
              <strong>{node.title}</strong>
              {node.suggested && <small>AI suggestion</small>}
            </span>
            <div>
              {node.suggested && (
                <button
                  aria-label={`Accept ${node.title}`}
                  onClick={() => onAcceptSuggestion?.(node)}
                  type="button"
                >
                  <Check aria-hidden="true" />
                </button>
              )}
              <button
                aria-label={`Move ${node.title} up`}
                disabled={index === 0}
                onClick={() => move(index, -1)}
                type="button"
              >
                <ArrowUp aria-hidden="true" />
              </button>
              <button
                aria-label={`Move ${node.title} down`}
                disabled={index === items.length - 1}
                onClick={() => move(index, 1)}
                type="button"
              >
                <ArrowDown aria-hidden="true" />
              </button>
              <button
                aria-label={`Remove ${node.title}`}
                onClick={() => remove(node.id)}
                type="button"
              >
                <X aria-hidden="true" />
              </button>
            </div>
          </li>
        ))}
      </ol>
      <Button
        onClick={() => {
          const next = [
            ...items,
            { id: `${Date.now()}`, title: "Untitled section", level: 2 as const }
          ];
          setItems(next);
          onChange?.(next);
        }}
        variant="secondary"
      >
        <Plus aria-hidden="true" />
        Add section
      </Button>
    </div>
  );
}

export interface ClientWorkspace {
  id: string;
  name: string;
  domain?: string;
  logoUrl?: string;
  credits?: number;
  documentCount?: number;
}
export interface ClientWorkspaceSwitcherProps extends Omit<
  HTMLAttributes<HTMLDivElement>,
  "onChange"
> {
  workspaces?: readonly ClientWorkspace[];
  value?: string;
  onChange?: (workspace: ClientWorkspace) => void;
  onCreate?: () => void;
}
const sampleWorkspaces: readonly ClientWorkspace[] = [
  { id: "amro", name: "Amro", domain: "amro.ai", credits: 2400, documentCount: 84 },
  {
    id: "northstar",
    name: "Northstar Labs",
    domain: "northstar.example",
    credits: 860,
    documentCount: 26
  }
];
export function ClientWorkspaceSwitcher({
  className,
  onChange,
  onCreate,
  value: controlled,
  workspaces = sampleWorkspaces,
  ...props
}: ClientWorkspaceSwitcherProps) {
  const [open, setOpen] = useState(false);
  const [internal, setInternal] = useState(workspaces[0]?.id);
  const value = controlled ?? internal;
  const selected = workspaces.find((workspace) => workspace.id === value);
  return (
    <div className={cn("amro-workspace-switcher", className)} {...props}>
      <button aria-expanded={open} onClick={() => setOpen((current) => !current)} type="button">
        <span className="amro-workspace-switcher__logo">{selected?.name.slice(0, 1)}</span>
        <span>
          <strong>{selected?.name ?? "Select workspace"}</strong>
          <small>{selected?.domain ?? "Brand-isolated content"}</small>
        </span>
        <ChevronRight aria-hidden="true" />
      </button>
      {open && (
        <div role="listbox" aria-label="Client workspaces">
          {workspaces.map((workspace) => (
            <button
              aria-selected={workspace.id === value}
              key={workspace.id}
              onClick={() => {
                setInternal(workspace.id);
                onChange?.(workspace);
                setOpen(false);
              }}
              role="option"
              type="button"
            >
              <span className="amro-workspace-switcher__logo">{workspace.name.slice(0, 1)}</span>
              <span>
                <strong>{workspace.name}</strong>
                <small>
                  {workspace.documentCount ?? 0} documents ·{" "}
                  {(workspace.credits ?? 0).toLocaleString()} credits
                </small>
              </span>
              {workspace.id === value && <Check aria-hidden="true" />}
            </button>
          ))}
          {onCreate && (
            <Button onClick={onCreate} variant="tertiary">
              <Plus aria-hidden="true" />
              New client workspace
            </Button>
          )}
        </div>
      )}
    </div>
  );
}

export interface BrandProfile {
  name: string;
  logoUrl?: string;
  colors: readonly string[];
  headingFont: string;
  bodyFont: string;
  tone: readonly string[];
}
export interface ClientBrandCapsuleProps extends HTMLAttributes<HTMLDivElement> {
  brand?: BrandProfile;
  onEdit?: () => void;
}
const sampleBrand: BrandProfile = {
  name: "Northstar Labs",
  colors: ["accent-teal", "accent-cyan", "surface-strong"],
  headingFont: "Display Sans",
  bodyFont: "Workhorse Sans",
  tone: ["Expert", "Direct", "Warm"]
};
export function ClientBrandCapsule({
  brand = sampleBrand,
  className,
  onEdit,
  ...props
}: ClientBrandCapsuleProps) {
  return (
    <Card className={cn("amro-brand-capsule", className)} {...props}>
      <header>
        <span className="amro-brand-capsule__logo">
          {brand.logoUrl ? <img alt="" src={brand.logoUrl} /> : brand.name.slice(0, 1)}
        </span>
        <span>
          <strong>{brand.name}</strong>
          <small>Active brand profile</small>
        </span>
        <StatusIndicator label="Applied" status="success" />
      </header>
      <div>
        <span>
          <small>Colours</small>
          <span>
            {brand.colors.map((color) => (
              <i data-color={color} key={color} title={color} />
            ))}
          </span>
        </span>
        <span>
          <small>Typography</small>
          <strong>
            {brand.headingFont} / {brand.bodyFont}
          </strong>
        </span>
        <span>
          <small>Tone</small>
          <span>
            {brand.tone.map((tone) => (
              <Badge key={tone} tone="neutral">
                {tone}
              </Badge>
            ))}
          </span>
        </span>
      </div>
      {onEdit && (
        <Button onClick={onEdit} variant="secondary">
          Edit brand profile
        </Button>
      )}
    </Card>
  );
}

export interface VoiceDimension {
  id: string;
  label: ReactNode;
  score: number;
  guidance?: ReactNode;
}
export interface BrandVoiceMeterProps extends HTMLAttributes<HTMLDivElement> {
  score?: number;
  dimensions?: readonly VoiceDimension[];
  terminologyMatches?: number;
  terminologyIssues?: number;
}
const voiceDimensions: readonly VoiceDimension[] = [
  { id: "tone", label: "Tone", score: 94, guidance: "Expert and direct" },
  { id: "clarity", label: "Clarity", score: 91, guidance: "Short, active sentences" },
  { id: "terms", label: "Terminology", score: 86, guidance: "2 preferred terms missing" }
];
export function BrandVoiceMeter({
  className,
  dimensions = voiceDimensions,
  score = 91,
  terminologyIssues = 2,
  terminologyMatches = 18,
  ...props
}: BrandVoiceMeterProps) {
  return (
    <FeaturePanel
      className={cn("amro-brand-voice", className)}
      eyebrow="Brand alignment"
      title={`${score}% aligned`}
      description={`${terminologyMatches} preferred terms matched · ${terminologyIssues} issues`}
      status={
        <StatusIndicator
          label={score >= 90 ? "On brand" : "Review tone"}
          status={score >= 90 ? "success" : "warning"}
        />
      }
      {...props}
    >
      <ul>
        {dimensions.map((dimension) => (
          <li key={dimension.id}>
            <span>
              <strong>{dimension.label}</strong>
              <small>{dimension.guidance}</small>
            </span>
            <meter max="100" value={dimension.score}>
              {dimension.score}%
            </meter>
            <strong>{dimension.score}</strong>
          </li>
        ))}
      </ul>
    </FeaturePanel>
  );
}

export interface ArticleSection {
  id: string;
  title: string;
  content: string;
  status?: "draft" | "generating" | "locked" | "issue";
}
export interface EditorialQualityDimension {
  id: string;
  label: ReactNode;
  score: number;
  guidance?: ReactNode;
}
export interface ArticleWorkspaceProps extends HTMLAttributes<HTMLDivElement> {
  title?: string;
  sections?: readonly ArticleSection[];
  activeSectionId?: string;
  quality?: readonly EditorialQualityDimension[];
  onSectionChange?: (id: string) => void;
  onContentChange?: (id: string, content: string) => void;
  onSave?: () => void;
}
const sampleArticleSections: readonly ArticleSection[] = [
  {
    id: "intro",
    title: "What support automation means",
    content:
      "AI support automation coordinates repetitive service work while preserving a clear boundary for human judgment.",
    status: "draft"
  },
  {
    id: "safe",
    title: "What agents can safely handle",
    content:
      "Classification, approved-knowledge retrieval, and structured workflow steps are strong candidates when each outcome remains inspectable.",
    status: "locked"
  },
  {
    id: "approval",
    title: "The human approval boundary",
    content: "Consequential external actions should pause for review.",
    status: "issue"
  }
];
export function ArticleWorkspace({
  activeSectionId,
  className,
  onContentChange,
  onSave,
  onSectionChange,
  quality = [
    { id: "complete", label: "Completeness", score: 88 },
    { id: "readability", label: "Readability", score: 92 },
    { id: "seo", label: "SEO coverage", score: 81 }
  ],
  sections = sampleArticleSections,
  title = "AI support automation: a governed approach",
  ...props
}: ArticleWorkspaceProps) {
  const [internalActive, setInternalActive] = useState(sections[0]?.id);
  const activeId = activeSectionId ?? internalActive;
  const section = sections.find((item) => item.id === activeId);
  return (
    <div className={cn("amro-article-workspace", className)} {...props}>
      <aside>
        <header>
          <strong>Outline</strong>
          <Badge tone="neutral">{sections.length}</Badge>
        </header>
        <nav aria-label="Article outline">
          {sections.map((item, index) => (
            <button
              aria-current={item.id === activeId ? "true" : undefined}
              key={item.id}
              onClick={() => {
                setInternalActive(item.id);
                onSectionChange?.(item.id);
              }}
              type="button"
            >
              <span>{index + 1}</span>
              <span>
                <strong>{item.title}</strong>
                <small>{item.status}</small>
              </span>
            </button>
          ))}
        </nav>
      </aside>
      <main>
        <header>
          <span>
            <small>Article draft</small>
            <strong>{title}</strong>
          </span>
          <Button onClick={onSave} size="sm" variant="secondary">
            Save draft
          </Button>
        </header>
        {section ? (
          <article>
            <h2>{section.title}</h2>
            <Textarea
              aria-label={`Content for ${section.title}`}
              onChange={(event) => onContentChange?.(section.id, event.target.value)}
              value={section.content}
            />
          </article>
        ) : (
          <EmptyNotice title="Choose a section" />
        )}
      </main>
      <aside>
        <header>
          <strong>Quality</strong>
          <StatusIndicator label="Needs review" status="warning" />
        </header>
        <ul>
          {quality.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>
      </aside>
    </div>
  );
}

export type SectionAction = "generate" | "rewrite" | "lock" | "unlock";
export interface SectionGenerationControlProps extends HTMLAttributes<HTMLDivElement> {
  sectionTitle?: ReactNode;
  status?: "idle" | "generating" | "ready" | "locked" | "error";
  variants?: number;
  onAction?: (action: SectionAction, guidance?: string) => void;
}
export function SectionGenerationControl({
  className,
  onAction,
  sectionTitle = "The human approval boundary",
  status = "ready",
  variants = 2,
  ...props
}: SectionGenerationControlProps) {
  const [guidance, setGuidance] = useState("");
  const locked = status === "locked";
  return (
    <div className={cn("amro-section-control", className)} {...props}>
      <header>
        <span>
          <strong>{sectionTitle}</strong>
          <small>{locked ? "Protected from regeneration" : `${variants} generated variants`}</small>
        </span>
        <StatusIndicator
          label={status}
          status={
            status === "ready" || locked ? "success" : status === "generating" ? "loading" : status
          }
        />
      </header>
      <label>
        <span>Section guidance</span>
        <input
          disabled={locked}
          onChange={(event) => setGuidance(event.target.value)}
          placeholder="Add a fact, angle, or tone instruction…"
          value={guidance}
        />
      </label>
      <footer>
        <Button
          disabled={locked || status === "generating"}
          onClick={() => onAction?.("generate", guidance)}
        >
          <Sparkles aria-hidden="true" />
          Generate
        </Button>
        <Button
          disabled={locked || status === "generating"}
          onClick={() => onAction?.("rewrite", guidance)}
          variant="secondary"
        >
          <RefreshCw aria-hidden="true" />
          Rewrite
        </Button>
        <Button onClick={() => onAction?.(locked ? "unlock" : "lock")} variant="tertiary">
          <LockKeyhole aria-hidden="true" />
          {locked ? "Unlock" : "Lock"}
        </Button>
      </footer>
    </div>
  );
}

export type InlineAICommand = "shorten" | "expand" | "clarify" | "cite" | "tone" | "translate";
export interface InlineAICommandMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  open?: boolean;
  selection?: ReactNode;
  commands?: readonly InlineAICommand[];
  onSelect?: (command: InlineAICommand) => void;
  onClose?: () => void;
}
const commandLabels: Record<InlineAICommand, string> = {
  shorten: "Shorten",
  expand: "Expand",
  clarify: "Clarify",
  cite: "Add citation",
  tone: "Change tone",
  translate: "Translate"
};
export function InlineAICommandMenu({
  className,
  commands = ["shorten", "expand", "clarify", "cite", "tone", "translate"],
  onClose,
  onSelect,
  open = true,
  selection = "Consequential external actions",
  ...props
}: InlineAICommandMenuProps) {
  if (!open) return null;
  return (
    <div
      className={cn("amro-inline-ai-menu", className)}
      role="menu"
      aria-label="AI editing commands"
      {...props}
    >
      <header>
        <span>
          <Sparkles aria-hidden="true" />
          <small>Selected</small>
          <strong>{selection}</strong>
        </span>
        {onClose && (
          <button aria-label="Close AI commands" onClick={onClose} type="button">
            <X aria-hidden="true" />
          </button>
        )}
      </header>
      <div>
        {commands.map((command) => (
          <button key={command} onClick={() => onSelect?.(command)} role="menuitem" type="button">
            {commandLabels[command]}
            <ChevronRight aria-hidden="true" />
          </button>
        ))}
      </div>
    </div>
  );
}

export interface ClaimReference {
  id: string;
  title: ReactNode;
  url?: string;
  passage?: ReactNode;
  publishedAt?: ReactNode;
  authority?: number;
}
export interface ClaimVerificationCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  claim?: ReactNode;
  reference?: ClaimReference;
  status?: "verified" | "partial" | "unsupported" | "checking";
  onOpenSource?: (reference: ClaimReference) => void;
  onReplace?: () => void;
}
const sampleReference: ClaimReference = {
  id: "source",
  title: "Customer operations benchmark 2026",
  url: "research.example/report",
  passage: "Respondents reported a 31% reduction in repetitive handling time.",
  publishedAt: "12 Jun 2026",
  authority: 88
};
export function ClaimVerificationCard({
  claim = "Governed automation reduced repetitive handling time by 31%.",
  className,
  onOpenSource,
  onReplace,
  reference = sampleReference,
  status = "verified",
  ...props
}: ClaimVerificationCardProps) {
  return (
    <FeaturePanel
      className={cn("amro-claim-verification", className)}
      eyebrow="Claim verification"
      title={claim}
      status={
        <StatusIndicator
          label={status}
          status={
            status === "verified"
              ? "success"
              : status === "checking"
                ? "loading"
                : status === "partial"
                  ? "warning"
                  : "error"
          }
        />
      }
      {...props}
    >
      <blockquote>
        <Quote aria-hidden="true" />
        <p>{reference.passage}</p>
        <cite>
          {reference.title} · {reference.publishedAt}
        </cite>
      </blockquote>
      <div>
        <Badge tone="neutral">Authority {reference.authority ?? "—"}/100</Badge>
        {onReplace && (
          <Button onClick={onReplace} variant="tertiary">
            Replace source
          </Button>
        )}
        {onOpenSource && (
          <Button onClick={() => onOpenSource(reference)} variant="secondary">
            Open source
            <Globe2 aria-hidden="true" />
          </Button>
        )}
      </div>
    </FeaturePanel>
  );
}

export interface CitationInspectorProps extends HTMLAttributes<HTMLDivElement> {
  reference?: ClaimReference;
  selectedPassage?: ReactNode;
  status?: "current" | "stale" | "low-quality";
  onUsePassage?: () => void;
  onOpenOriginal?: () => void;
}
export function CitationInspector({
  className,
  onOpenOriginal,
  onUsePassage,
  reference = sampleReference,
  selectedPassage,
  status = "current",
  ...props
}: CitationInspectorProps) {
  return (
    <div className={cn("amro-citation-inspector", className)} {...props}>
      <header>
        <span>
          <strong>{reference.title}</strong>
          <small>{reference.url}</small>
        </span>
        <StatusIndicator label={status} status={status === "current" ? "success" : "warning"} />
      </header>
      <dl>
        <div>
          <dt>Published</dt>
          <dd>{reference.publishedAt}</dd>
        </div>
        <div>
          <dt>Source quality</dt>
          <dd>{reference.authority ?? "—"}/100</dd>
        </div>
      </dl>
      <blockquote>{selectedPassage ?? reference.passage}</blockquote>
      <footer>
        {onOpenOriginal && (
          <Button onClick={onOpenOriginal} variant="tertiary">
            Open original
          </Button>
        )}
        {onUsePassage && (
          <Button onClick={onUsePassage}>
            Use passage
            <Link2 aria-hidden="true" />
          </Button>
        )}
      </footer>
    </div>
  );
}

export interface TrustPatternWarningProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  kind?: "statistic" | "quote" | "attribution";
  title?: ReactNode;
  excerpt?: ReactNode;
  reason?: ReactNode;
  blocking?: boolean;
  onFindSource?: () => void;
  onRemove?: () => void;
}
export function TrustPatternWarning({
  blocking = true,
  className,
  excerpt = "Teams save up to 60% of support time.",
  kind = "statistic",
  onFindSource,
  onRemove,
  reason = "No approved source supports this number.",
  title = "Unsupported statistic",
  ...props
}: TrustPatternWarningProps) {
  return (
    <div
      className={cn("amro-trust-warning", className)}
      role={blocking ? "alert" : "status"}
      {...props}
    >
      <CircleAlert aria-hidden="true" />
      <span>
        <span className="amro-eyebrow">
          {blocking ? "Publication blocked" : "Review recommended"} · {kind}
        </span>
        <strong>{title}</strong>
        <blockquote>{excerpt}</blockquote>
        <small>{reason}</small>
      </span>
      <div>
        {onRemove && (
          <Button onClick={onRemove} variant="tertiary">
            Remove claim
          </Button>
        )}
        {onFindSource && <Button onClick={onFindSource}>Find supporting source</Button>}
      </div>
    </div>
  );
}

export interface ContentQualityPanelProps extends HTMLAttributes<HTMLDivElement> {
  dimensions?: readonly EditorialQualityDimension[];
  overall?: number;
  mandatoryIssues?: number;
  warnings?: number;
  onOpenDimension?: (dimension: EditorialQualityDimension) => void;
}
const contentQuality: readonly EditorialQualityDimension[] = [
  { id: "complete", label: "Completeness", score: 94 },
  { id: "readability", label: "Readability", score: 91 },
  { id: "seo", label: "SEO", score: 84 },
  { id: "citations", label: "Citations", score: 78 },
  { id: "brand", label: "Brand alignment", score: 93 }
];
export function ContentQualityPanel({
  className,
  dimensions = contentQuality,
  mandatoryIssues = 1,
  onOpenDimension,
  overall = 88,
  warnings = 3,
  ...props
}: ContentQualityPanelProps) {
  return (
    <FeaturePanel
      className={cn("amro-content-quality", className)}
      eyebrow="Quality review"
      title={`${overall}/100`}
      description={`${mandatoryIssues} blocking issue · ${warnings} warnings`}
      status={
        <StatusIndicator
          label={mandatoryIssues ? "Not ready" : "Ready"}
          status={mandatoryIssues ? "blocked" : "success"}
        />
      }
      {...props}
    >
      <ul>
        {dimensions.map((dimension) => (
          <li key={dimension.id}>
            <button onClick={() => onOpenDimension?.(dimension)} type="button">
              <span>
                <strong>{dimension.label}</strong>
                <small>
                  {dimension.score >= 90
                    ? "Strong"
                    : dimension.score >= 80
                      ? "Review"
                      : "Needs work"}
                </small>
              </span>
              <meter max="100" value={dimension.score}>
                {dimension.score}%
              </meter>
              <strong>{dimension.score}</strong>
              <ChevronRight aria-hidden="true" />
            </button>
          </li>
        ))}
      </ul>
    </FeaturePanel>
  );
}

export interface EditorialIssue {
  id: string;
  title: ReactNode;
  detail?: ReactNode;
  section?: ReactNode;
  severity: "blocking" | "warning" | "suggestion";
  resolved?: boolean;
}
export interface IssueNavigatorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  issues?: readonly EditorialIssue[];
  activeId?: string;
  onSelect?: (issue: EditorialIssue) => void;
  onResolve?: (issue: EditorialIssue) => void;
}
const sampleIssues: readonly EditorialIssue[] = [
  {
    id: "unsupported",
    title: "Unsupported statistic",
    detail: "60% time-saving claim needs evidence",
    section: "Introduction",
    severity: "blocking"
  },
  {
    id: "keyword",
    title: "Missing semantic term",
    detail: "Add “agent governance” naturally",
    section: "Safety",
    severity: "warning"
  },
  {
    id: "sentence",
    title: "Long sentence",
    detail: "42 words",
    section: "Workflow",
    severity: "suggestion"
  }
];
export function IssueNavigator({
  activeId,
  className,
  issues = sampleIssues,
  onResolve,
  onSelect,
  ...props
}: IssueNavigatorProps) {
  const unresolved = issues.filter((issue) => !issue.resolved);
  return (
    <div className={cn("amro-issue-navigator", className)} {...props}>
      <header>
        <span>
          <strong>Issues</strong>
          <small>{unresolved.length} unresolved</small>
        </span>
        <Badge
          tone={unresolved.some((issue) => issue.severity === "blocking") ? "info" : "neutral"}
        >
          {unresolved.filter((issue) => issue.severity === "blocking").length} blocking
        </Badge>
      </header>
      {unresolved.length ? (
        <ol>
          {unresolved.map((issue, index) => (
            <li data-active={issue.id === activeId} data-severity={issue.severity} key={issue.id}>
              <button onClick={() => onSelect?.(issue)} type="button">
                <span>{index + 1}</span>
                <span>
                  <strong>{issue.title}</strong>
                  <small>
                    {issue.section} · {issue.detail}
                  </small>
                </span>
                <ChevronRight aria-hidden="true" />
              </button>
              {onResolve && (
                <button
                  aria-label={`Resolve ${getAccessibleText(issue.title, "editorial issue")}`}
                  onClick={() => onResolve(issue)}
                  type="button"
                >
                  <Check aria-hidden="true" />
                </button>
              )}
            </li>
          ))}
        </ol>
      ) : (
        <EmptyNotice
          title="All issues resolved"
          description="The article is ready for its publication gate."
        />
      )}
    </div>
  );
}

export interface AIImageAsset {
  id: string;
  src: string;
  alt?: string;
  caption?: string;
  prompt?: string;
}
export interface AIImageSlotProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  asset?: AIImageAsset;
  status?: "empty" | "generating" | "ready" | "error";
  defaultPrompt?: string;
  onGenerate?: (prompt: string) => void;
  onReplace?: () => void;
  onChange?: (asset: AIImageAsset) => void;
}
export function AIImageSlot({
  asset,
  className,
  defaultPrompt = "A calm editorial illustration of a governed AI support workflow",
  onChange,
  onGenerate,
  onReplace,
  status = asset ? "ready" : "empty",
  ...props
}: AIImageSlotProps) {
  const [prompt, setPrompt] = useState(asset?.prompt ?? defaultPrompt);
  const [caption, setCaption] = useState(asset?.caption ?? "");
  const [alt, setAlt] = useState(asset?.alt ?? "");
  const update = (patch: Partial<AIImageAsset>) => {
    if (asset) onChange?.({ ...asset, ...patch });
  };
  return (
    <div className={cn("amro-ai-image-slot", className)} data-status={status} {...props}>
      <div className="amro-ai-image-slot__preview">
        {asset ? (
          <img alt={alt} src={asset.src} />
        ) : status === "generating" ? (
          <span>
            <Sparkles aria-hidden="true" />
            Generating editorial image…
          </span>
        ) : (
          <span>
            <ImageIcon aria-hidden="true" />
            No image generated
          </span>
        )}
        <StatusIndicator
          label={status}
          status={status === "ready" ? "success" : status === "generating" ? "loading" : status}
        />
      </div>
      <label>
        <span>Image prompt</span>
        <Textarea onChange={(event) => setPrompt(event.target.value)} value={prompt} />
      </label>
      <div>
        <Input
          label="Caption"
          onChange={(event) => {
            setCaption(event.target.value);
            update({ caption: event.target.value });
          }}
          value={caption}
        />
        <Input
          label="Alt text"
          onChange={(event) => {
            setAlt(event.target.value);
            update({ alt: event.target.value });
          }}
          value={alt}
        />
      </div>
      <footer>
        {onReplace && (
          <Button onClick={onReplace} variant="secondary">
            <Upload aria-hidden="true" />
            Replace
          </Button>
        )}
        <Button
          disabled={!prompt.trim() || status === "generating"}
          onClick={() => onGenerate?.(prompt)}
        >
          <RefreshCw aria-hidden="true" />
          {asset ? "Regenerate" : "Generate image"}
        </Button>
      </footer>
    </div>
  );
}

export interface FigureQualityCardProps extends HTMLAttributes<HTMLDivElement> {
  relevance?: number;
  captionComplete?: boolean;
  altTextComplete?: boolean;
  placement?: "good" | "review" | "poor";
  issues?: readonly ReactNode[];
  onFix?: () => void;
}
export function FigureQualityCard({
  altTextComplete = true,
  captionComplete = true,
  className,
  issues = [],
  onFix,
  placement = "good",
  relevance = 94,
  ...props
}: FigureQualityCardProps) {
  const ready =
    relevance >= 80 &&
    captionComplete &&
    altTextComplete &&
    placement === "good" &&
    issues.length === 0;
  return (
    <FeaturePanel
      className={cn("amro-figure-quality", className)}
      eyebrow="Figure quality"
      title={`${relevance}/100 relevance`}
      status={
        <StatusIndicator
          label={ready ? "Ready" : "Needs work"}
          status={ready ? "success" : "warning"}
        />
      }
      {...props}
    >
      <ul>
        <li data-pass={relevance >= 80}>
          <ImageIcon aria-hidden="true" />
          <span>
            <strong>Article relevance</strong>
            <small>{relevance}% semantic match</small>
          </span>
        </li>
        <li data-pass={captionComplete}>
          <BookOpenCheck aria-hidden="true" />
          <span>
            <strong>Caption</strong>
            <small>{captionComplete ? "Descriptive caption present" : "Caption missing"}</small>
          </span>
        </li>
        <li data-pass={altTextComplete}>
          <Accessibility aria-hidden="true" />
          <span>
            <strong>Accessibility</strong>
            <small>{altTextComplete ? "Alt text present" : "Alt text missing"}</small>
          </span>
        </li>
        <li data-pass={placement === "good"}>
          <MapPin aria-hidden="true" />
          <span>
            <strong>Placement</strong>
            <small>
              {placement === "good" ? "Supports nearby content" : "Review article position"}
            </small>
          </span>
        </li>
      </ul>
      {issues.length > 0 && (
        <div>
          {issues.map((issue, index) => (
            <span key={index}>
              <CircleAlert aria-hidden="true" />
              {issue}
            </span>
          ))}
        </div>
      )}
      {onFix && !ready && <Button onClick={onFix}>Fix figure issues</Button>}
    </FeaturePanel>
  );
}

export interface LinkSuggestion {
  id: string;
  destination: string;
  title: ReactNode;
  anchor: string;
  relevance: number;
  reason?: ReactNode;
}
export interface InternalLinkSuggestionProps extends HTMLAttributes<HTMLDivElement> {
  suggestion?: LinkSuggestion;
  onInsert?: (suggestion: LinkSuggestion) => void;
  onDismiss?: (suggestion: LinkSuggestion) => void;
}
const sampleLink: LinkSuggestion = {
  id: "link",
  destination: "/components/human-approval-pipeline",
  title: "Human approval pipeline",
  anchor: "human approval boundary",
  relevance: 94,
  reason: "The destination explains the governance pattern referenced in this paragraph."
};
export function InternalLinkSuggestion({
  className,
  onDismiss,
  onInsert,
  suggestion = sampleLink,
  ...props
}: InternalLinkSuggestionProps) {
  return (
    <Card className={cn("amro-internal-link", className)} {...props}>
      <header>
        <Link2 aria-hidden="true" />
        <span>
          <span className="amro-eyebrow">Internal link suggestion</span>
          <strong>{suggestion.title}</strong>
          <small>{suggestion.destination}</small>
        </span>
        <Badge tone="success">{suggestion.relevance}% relevant</Badge>
      </header>
      <p>{suggestion.reason}</p>
      <div>
        <span>Suggested anchor</span>
        <code>{suggestion.anchor}</code>
      </div>
      <footer>
        {onDismiss && (
          <Button onClick={() => onDismiss(suggestion)} variant="tertiary">
            Dismiss
          </Button>
        )}
        {onInsert && <Button onClick={() => onInsert(suggestion)}>Insert link</Button>}
      </footer>
    </Card>
  );
}

export interface ArticleMetadata {
  title: string;
  description: string;
  slug: string;
}
export interface MetadataPreviewProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
  value?: ArticleMetadata;
  siteName?: ReactNode;
  domain?: string;
  onChange?: (metadata: ArticleMetadata) => void;
}
export function MetadataPreview({
  className,
  domain = "amro.ai",
  onChange,
  siteName = "Amro",
  value = {
    title: "AI support automation: a governed approach",
    description:
      "Learn where AI support agents create value, where human approval belongs, and how to measure verified outcomes.",
    slug: "ai-support-automation"
  },
  ...props
}: MetadataPreviewProps) {
  const [metadata, setMetadata] = useState(value);
  const update = <K extends keyof ArticleMetadata>(key: K, next: ArticleMetadata[K]) => {
    const updated = { ...metadata, [key]: next };
    setMetadata(updated);
    onChange?.(updated);
  };
  return (
    <div className={cn("amro-metadata-preview", className)} {...props}>
      <section>
        <span className="amro-eyebrow">Search preview</span>
        <div>
          <small>
            {siteName} · {domain}/{metadata.slug}
          </small>
          <strong>{metadata.title}</strong>
          <p>{metadata.description}</p>
        </div>
      </section>
      <form>
        <Input
          label="SEO title"
          maxLength={60}
          value={metadata.title}
          onChange={(event) => update("title", event.target.value)}
        />
        <small>{metadata.title.length}/60</small>
        <label>
          <span>Meta description</span>
          <Textarea
            maxLength={160}
            onChange={(event) => update("description", event.target.value)}
            value={metadata.description}
          />
        </label>
        <small>{metadata.description.length}/160</small>
        <Input
          label="URL slug"
          value={metadata.slug}
          onChange={(event) => update("slug", event.target.value)}
        />
      </form>
    </div>
  );
}

export interface ContentVersion {
  id: string;
  label: ReactNode;
  author: ReactNode;
  timestamp: ReactNode;
  kind: "generated" | "edited" | "reviewed" | "exported";
  current?: boolean;
}
export interface ContentVersionTimelineProps extends HTMLAttributes<HTMLOListElement> {
  versions?: readonly ContentVersion[];
  onRestore?: (version: ContentVersion) => void;
  onCompare?: (version: ContentVersion) => void;
}
const sampleVersions: readonly ContentVersion[] = [
  {
    id: "v4",
    label: "Ready for publication",
    author: "Maya",
    timestamp: "Today, 11:42",
    kind: "reviewed",
    current: true
  },
  { id: "v3", label: "Editorial edits", author: "Maya", timestamp: "Today, 11:18", kind: "edited" },
  {
    id: "v2",
    label: "Quality rewrite",
    author: "AmroPilot",
    timestamp: "Today, 10:56",
    kind: "generated"
  },
  {
    id: "v1",
    label: "Initial draft",
    author: "AmroPilot",
    timestamp: "Today, 10:31",
    kind: "generated"
  }
];
export function ContentVersionTimeline({
  className,
  onCompare,
  onRestore,
  versions = sampleVersions,
  ...props
}: ContentVersionTimelineProps) {
  return (
    <ol className={cn("amro-version-timeline", className)} {...props}>
      {versions.map((version) => (
        <li data-kind={version.kind} key={version.id}>
          <span>
            {version.current ? <CheckCircle2 aria-hidden="true" /> : <Clock3 aria-hidden="true" />}
          </span>
          <span>
            <strong>{version.label}</strong>
            <small>
              {version.author} · {version.timestamp}
            </small>
          </span>
          {version.current ? (
            <Badge tone="success">Current</Badge>
          ) : (
            <div>
              {onCompare && (
                <Button onClick={() => onCompare(version)} size="sm" variant="tertiary">
                  Compare
                </Button>
              )}
              {onRestore && (
                <Button onClick={() => onRestore(version)} size="sm" variant="secondary">
                  Restore
                </Button>
              )}
            </div>
          )}
        </li>
      ))}
    </ol>
  );
}

export interface ReviewComment {
  id: string;
  author: ReactNode;
  text: ReactNode;
  selection?: ReactNode;
  status?: "open" | "accepted" | "rejected";
}
export interface EditorReviewModeProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  comments?: readonly ReviewComment[];
  activeId?: string;
  onSelect?: (comment: ReviewComment) => void;
  onDecision?: (comment: ReviewComment, decision: "accept" | "reject") => void;
  onAddComment?: (text: string) => void;
}
const sampleComments: readonly ReviewComment[] = [
  {
    id: "one",
    author: "Maya Chen",
    text: "Can we support this with the benchmark source?",
    selection: "reduced repetitive handling time",
    status: "open"
  },
  {
    id: "two",
    author: "Alex Morgan",
    text: "Use the approved term “control boundary”.",
    selection: "approval step",
    status: "open"
  }
];
export function EditorReviewMode({
  activeId,
  className,
  comments = sampleComments,
  onAddComment,
  onDecision,
  onSelect,
  ...props
}: EditorReviewModeProps) {
  const [draft, setDraft] = useState("");
  return (
    <div className={cn("amro-review-mode", className)} {...props}>
      <header>
        <span>
          <MessageSquare aria-hidden="true" />
          <strong>Review comments</strong>
        </span>
        <Badge tone="neutral">
          {comments.filter((comment) => comment.status === "open").length} open
        </Badge>
      </header>
      <ol>
        {comments.map((comment) => (
          <li data-active={comment.id === activeId} key={comment.id}>
            <button onClick={() => onSelect?.(comment)} type="button">
              <span>
                <strong>{comment.author}</strong>
                <Badge tone={comment.status === "accepted" ? "success" : "neutral"}>
                  {comment.status}
                </Badge>
              </span>
              {comment.selection && <blockquote>{comment.selection}</blockquote>}
              <p>{comment.text}</p>
            </button>
            {comment.status === "open" && onDecision && (
              <footer>
                <Button onClick={() => onDecision(comment, "reject")} size="sm" variant="tertiary">
                  Reject
                </Button>
                <Button onClick={() => onDecision(comment, "accept")} size="sm">
                  Accept
                </Button>
              </footer>
            )}
          </li>
        ))}
      </ol>
      {onAddComment && (
        <form
          onSubmit={(event) => {
            event.preventDefault();
            if (draft.trim()) {
              onAddComment(draft);
              setDraft("");
            }
          }}
        >
          <input
            aria-label="Add review comment"
            onChange={(event) => setDraft(event.target.value)}
            placeholder="Add a comment…"
            value={draft}
          />
          <Button disabled={!draft.trim()} size="sm" type="submit">
            Comment
          </Button>
        </form>
      )}
    </div>
  );
}

export type ExportFormat = "pdf" | "docx" | "html" | "cms";
export interface ExportFormatOption {
  id: ExportFormat;
  label: ReactNode;
  detail: ReactNode;
  ready?: boolean;
}
export interface ExportFormatTrayProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
  formats?: readonly ExportFormatOption[];
  onSelect?: (format: ExportFormat) => void;
  disabled?: boolean;
}
const exportFormats: readonly ExportFormatOption[] = [
  { id: "pdf", label: "PDF", detail: "Branded, print-ready", ready: true },
  { id: "docx", label: "DOCX", detail: "Editable document", ready: true },
  { id: "html", label: "HTML", detail: "Clean semantic markup", ready: true },
  { id: "cms", label: "CMS payload", detail: "Webhook delivery", ready: false }
];
export function ExportFormatTray({
  className,
  disabled = false,
  formats = exportFormats,
  onSelect,
  ...props
}: ExportFormatTrayProps) {
  return (
    <div className={cn("amro-export-tray", className)} {...props}>
      <header>
        <span>
          <strong>Export article</strong>
          <small>Select a production format</small>
        </span>
        <StatusIndicator
          label={disabled ? "Blocked by quality gate" : "Ready to export"}
          status={disabled ? "blocked" : "success"}
        />
      </header>
      <div>
        {formats.map((format) => (
          <button
            disabled={disabled || format.ready === false}
            key={format.id}
            onClick={() => onSelect?.(format.id)}
            type="button"
          >
            <span>
              {format.id === "html" || format.id === "cms" ? (
                <Code2 aria-hidden="true" />
              ) : (
                <FileText aria-hidden="true" />
              )}
            </span>
            <span>
              <strong>{format.label}</strong>
              <small>{format.detail}</small>
            </span>
            <Download aria-hidden="true" />
          </button>
        ))}
      </div>
    </div>
  );
}

export interface BrandedExportPreviewProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  brand?: BrandProfile;
  title?: ReactNode;
  author?: ReactNode;
  sections?: readonly { heading: ReactNode; body: ReactNode }[];
  page?: number;
  pages?: number;
  onPageChange?: (page: number) => void;
}
export function BrandedExportPreview({
  author = "Amro editorial team",
  brand = sampleBrand,
  className,
  onPageChange,
  page = 1,
  pages = 4,
  sections = [
    {
      heading: "What support automation means",
      body: "AI support automation coordinates repetitive service work while preserving a visible boundary for human judgment."
    },
    {
      heading: "Where governance belongs",
      body: "Approval gates keep consequential external actions under human control."
    }
  ],
  title = "AI support automation: a governed approach",
  ...props
}: BrandedExportPreviewProps) {
  return (
    <div className={cn("amro-branded-preview", className)} {...props}>
      <header>
        <span>
          <Eye aria-hidden="true" />
          <strong>Branded export preview</strong>
        </span>
        <span>
          Page {page} of {pages}
        </span>
      </header>
      <article>
        <div className="amro-branded-preview__brand">
          <span>{brand.name.slice(0, 1)}</span>
          <strong>{brand.name}</strong>
        </div>
        <h1>{title}</h1>
        <p className="amro-branded-preview__byline">By {author}</p>
        {sections.map((section, index) => (
          <section key={index}>
            <h2>{section.heading}</h2>
            <p>{section.body}</p>
          </section>
        ))}
        <footer>
          {brand.name} · {page}
        </footer>
      </article>
      <nav aria-label="Preview pages">
        <Button disabled={page <= 1} onClick={() => onPageChange?.(page - 1)} variant="secondary">
          Previous
        </Button>
        <Button
          disabled={page >= pages}
          onClick={() => onPageChange?.(page + 1)}
          variant="secondary"
        >
          Next
        </Button>
      </nav>
    </div>
  );
}

export interface WebhookTestResult {
  status: number;
  durationMs: number;
  response: ReactNode;
}
export interface CMSWebhookTesterProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
  defaultEndpoint?: string;
  payload?: string;
  state?: "idle" | "testing" | "success" | "error";
  result?: WebhookTestResult;
  onSubmit?: (endpoint: string, payload: string) => void;
}
export function CMSWebhookTester({
  className,
  defaultEndpoint = "https://cms.example/api/articles",
  onSubmit,
  payload = '{\n  "title": "AI support automation",\n  "status": "draft"\n}',
  result = { status: 201, durationMs: 284, response: "Article draft created" },
  state = "idle",
  ...props
}: CMSWebhookTesterProps) {
  const [endpoint, setEndpoint] = useState(defaultEndpoint);
  const [body, setBody] = useState(payload);
  return (
    <form
      className={cn("amro-webhook-tester", className)}
      onSubmit={(event) => {
        event.preventDefault();
        onSubmit?.(endpoint, body);
      }}
      {...props}
    >
      <header>
        <span>
          <Webhook aria-hidden="true" />
          <span>
            <strong>CMS webhook tester</strong>
            <small>Send a non-publishing test payload</small>
          </span>
        </span>
        <StatusIndicator label={state} status={state === "testing" ? "loading" : state} />
      </header>
      <Input
        label="Endpoint"
        type="url"
        value={endpoint}
        onChange={(event) => setEndpoint(event.target.value)}
      />
      <label>
        <span>Payload preview</span>
        <Textarea
          onChange={(event) => setBody(event.target.value)}
          spellCheck="false"
          value={body}
        />
      </label>
      {state === "success" || state === "error" ? (
        <div data-state={state}>
          <strong>
            HTTP {result.status} · {result.durationMs} ms
          </strong>
          <code>{result.response}</code>
        </div>
      ) : null}
      <footer>
        <Button disabled={!endpoint.trim() || state === "testing"} type="submit">
          Test endpoint
          <Webhook aria-hidden="true" />
        </Button>
      </footer>
    </form>
  );
}

export interface CreditForecastItem {
  id: string;
  label: ReactNode;
  credits: number;
  required?: boolean;
}
export interface CreditForecastProps extends HTMLAttributes<HTMLDivElement> {
  items?: readonly CreditForecastItem[];
  balance?: number;
  onGenerate?: () => void;
}
const forecastItems: readonly CreditForecastItem[] = [
  { id: "research", label: "SERP research", credits: 120, required: true },
  { id: "draft", label: "Article generation", credits: 420, required: true },
  { id: "images", label: "3 editorial images", credits: 180 },
  { id: "quality", label: "Quality and citation review", credits: 90, required: true }
];
export function CreditForecast({
  balance = 1200,
  className,
  items = forecastItems,
  onGenerate,
  ...props
}: CreditForecastProps) {
  const total = items.reduce((sum, item) => sum + item.credits, 0);
  const enough = balance >= total;
  return (
    <FeaturePanel
      className={cn("amro-credit-forecast", className)}
      eyebrow="Before generation"
      title={`${total.toLocaleString()} credits estimated`}
      description={`${balance.toLocaleString()} available after workspace limits`}
      status={
        <StatusIndicator
          label={enough ? "Within balance" : "Insufficient balance"}
          status={enough ? "success" : "blocked"}
        />
      }
      {...props}
    >
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <span>
              {item.label}
              {item.required && <small>Required</small>}
            </span>
            <strong>{item.credits}</strong>
          </li>
        ))}
      </ul>
      <footer>
        <span>
          Estimated remaining <strong>{Math.max(0, balance - total).toLocaleString()}</strong>
        </span>
        {onGenerate && (
          <Button disabled={!enough} onClick={onGenerate}>
            Generate article
            <Sparkles aria-hidden="true" />
          </Button>
        )}
      </footer>
    </FeaturePanel>
  );
}

export type ArticleStatus = "researching" | "drafting" | "review" | "ready" | "published";
export interface ArticleStatusCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
  status?: ArticleStatus;
  title?: ReactNode;
  progress?: number;
  updatedAt?: ReactNode;
  owner?: ReactNode;
  issues?: number;
  onOpen?: () => void;
}
const articleStatusLabel: Record<ArticleStatus, string> = {
  researching: "Researching",
  drafting: "Drafting",
  review: "Needs review",
  ready: "Ready",
  published: "Published"
};
export function ArticleStatusCard({
  className,
  issues = 1,
  onOpen,
  owner = "Maya Chen",
  progress = 88,
  status = "review",
  title = "AI support automation: a governed approach",
  updatedAt = "Updated 4 min ago",
  ...props
}: ArticleStatusCardProps) {
  return (
    <Card className={cn("amro-article-status", className)} {...props}>
      <header>
        <span>
          <FileText aria-hidden="true" />
        </span>
        <span>
          <strong>{title}</strong>
          <small>
            {owner} · {updatedAt}
          </small>
        </span>
        <StatusIndicator
          label={articleStatusLabel[status]}
          status={
            status === "published" || status === "ready"
              ? "success"
              : status === "review"
                ? "warning"
                : "active"
          }
        />
      </header>
      <div>
        <progress max="100" value={progress}>
          {progress}%
        </progress>
        <strong>{progress}%</strong>
      </div>
      <footer>
        <span>
          {issues ? (
            <>
              <CircleAlert aria-hidden="true" />
              {issues} unresolved issue
            </>
          ) : (
            <>
              <Check aria-hidden="true" />
              Quality gate passed
            </>
          )}
        </span>
        {onOpen && (
          <Button onClick={onOpen} variant="secondary">
            Open article
            <ChevronRight aria-hidden="true" />
          </Button>
        )}
      </footer>
    </Card>
  );
}
```



## Usage

Shared product source installed automatically by AmroUI component entries.

