All files / packages/sds-customization-compliance/src/components CompliancePanel.tsx

100% Statements 17/17
94.73% Branches 18/19
100% Functions 7/7
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148                                                                                              9x 22x   22x 22x   22x                                             12x 1x                       11x 1x             10x 1x             9x 9x 9x   9x                                           32x               22x                
import { COMPLIANCE_STATUS_PRIORITY, getVerdict } from '../types.js';
import type { ComplianceRow as ComplianceRowData } from '../types.js';
import { ComplianceRow, type InlineRenderer } from './ComplianceRow.js';
import { SUMMARY_ORDER, VERDICT_COPY, verdictSubtitle } from './copy.js';
 
export interface CompliancePanelProps {
  /**
   * Rows to render, in the order they should appear. Callers can pass the
   * full `rows` array from the per-component report, or a filtered subset.
   */
  rows?: ComplianceRowData[];
  /** Optional component name, surfaced via `data-component`. */
  componentName?: string;
  /** Loading state — renders a visual placeholder, no rows. */
  loading?: boolean;
  /** Error state — renders an error banner, no rows. */
  error?: string | null;
  /** Fallback message when there are no rows (and no error/loading). */
  emptyLabel?: string;
  /**
   * When true, render a summary strip with pass/fail/info counts above the
   * row list. Defaults to true; opt out for embedded contexts where the host
   * draws its own summary.
   */
  showSummary?: boolean;
  /**
   * When true, render a verdict header above the summary strip. Defaults to
   * true. Opt out for embedded contexts where the host draws its own
   * "is this component conformant?" indicator.
   */
  showVerdict?: boolean;
  /** Forwarded to each row (see `ComplianceRow.maxOffenders`). */
  maxOffendersPerRow?: number;
  /**
   * Forwarded to each row. Lets the host supply its own markdown/inline
   * renderer for `row.label` and `row.detail`; without it, those fields
   * render as plain text (backticks and asterisks stay literal).
   */
  renderInline?: InlineRenderer;
}
 
/**
 * Stable sort by severity priority: fail → review → pass → info.
 * Ties keep the original (check-registration) order, so callers who
 * care about sub-ordering within a severity can pre-sort their rows.
 */
function sortByStatus(rows: ComplianceRowData[]): ComplianceRowData[] {
  return rows
    .map((row, index) => ({ row, index }))
    .sort((a, b) => {
      const priority = COMPLIANCE_STATUS_PRIORITY[a.row.status] - COMPLIANCE_STATUS_PRIORITY[b.row.status];
      return priority !== 0 ? priority : a.index - b.index;
    })
    .map((entry) => entry.row);
}
 
/**
 * Panel that renders a list of compliance rows with an optional summary strip.
 *
 * Consumers are expected to fetch the report themselves (e.g. from the
 * sandbox's `/api/compliance/<component>` route, or from the Storybook
 * static-dir copy) and pass `rows` in. Keeping fetch logic out of the
 * component lets the same panel mount in tests, MDX pages, and non-network
 * contexts.
 */
export function CompliancePanel({
  rows,
  componentName,
  loading = false,
  error = null,
  emptyLabel = 'No compliance data available.',
  showSummary = true,
  showVerdict = true,
  maxOffendersPerRow,
  renderInline,
}: CompliancePanelProps) {
  if (loading) {
    return (
      <div
        className="scp-panel scp-panel--loading"
        data-component={componentName}
        role="status"
        aria-live="polite"
      >
        Loading compliance data…
      </div>
    );
  }
 
  if (error) {
    return (
      <div className="scp-panel scp-panel--error" data-component={componentName} role="alert">
        {error}
      </div>
    );
  }
 
  if (!rows || rows.length === 0) {
    return (
      <div className="scp-panel scp-panel--empty" data-component={componentName}>
        {emptyLabel}
      </div>
    );
  }
 
  const { verdict, counts } = getVerdict(rows);
  const sortedRows = sortByStatus(rows);
  const verdictCopy = VERDICT_COPY[verdict];
 
  return (
    <div className="scp-panel" data-component={componentName} data-verdict={verdict}>
      {showVerdict ? (
        <div className="scp-panel__verdict" data-verdict={verdict} role="status" aria-live="polite">
          <span className="scp-panel__verdict-glyph" aria-hidden="true">
            {verdictCopy.glyph}
          </span>
          <span className="scp-panel__verdict-body">
            <span className="scp-panel__verdict-title">{verdictCopy.title}</span>
            <span className="scp-panel__verdict-subtitle">{verdictSubtitle(verdict, counts)}</span>
          </span>
        </div>
      ) : null}
      {showSummary ? (
        <div
          className="scp-panel__summary"
          data-pass={counts.pass}
          data-fail={counts.fail}
          data-review={counts.review}
          data-info={counts.info}
        >
          {SUMMARY_ORDER.map((status) => (
            <span key={status} className="scp-panel__summary-item" data-status={status}>
              {counts[status]} {status}
            </span>
          ))}
        </div>
      ) : null}
      <ul className="scp-panel__rows">
        {sortedRows.map((row) => (
          <li key={row.id} className="scp-panel__row">
            <ComplianceRow row={row} maxOffenders={maxOffendersPerRow} renderInline={renderInline} />
          </li>
        ))}
      </ul>
    </div>
  );
}