エクスクルーシブ
Cross-Check <mv-cross-check>
レビュー業務向けの書類間照合デスクです(KYC、請求書と発注書と納品書、契約書と見積書、フォームの送信内容と身分証明書)。
文化的リファレンス
Papers, Please、ルーカス・ポープ(2013年、ゲーム)。国境の検問所で、審査官はある書類の細部をクリックし、次に別の書類の対応する細部をクリックして矛盾を暴き、その証拠をもとに入国者一人ひとりを許可するか拒否するかを決めなければなりません。ここでは任意の書類一式がフィールドごとに照合され、不一致はその場でマークされます。審査担当者は任意の 2 つのフィールドを結び付けてチェックが見落としたものにフラグを立て、各矛盾を解決したうえで、理由を添えて承認または拒否の判定を記録します。
仕組み
審査ワークフロー(KYC、請求書と発注書と納品書、契約書と見積書、フォーム送信と身分証明書)向けの文書間照合デスクです。2 つ以上の文書を並べて配置します(フィールドに data-field="key" を持つ任意のマークアップ)。複数の文書に見つかったキーはすべて、キーごとのルール(完全一致、fold による大文字小文字とアクセントを区別しない比較、英数字のみの比較、絶対値または % による数値の許容差、03/14/1991 と 14 MAR 1991 のように書式が異なる日付の一致、または独自の関数)で基準文書と比較され、その場でマークされます。一致には点線の下線、不一致にはハイライトと番号付きのピップが付きます。インスペクトモードではすべてのフィールドがターゲットになります。あるフィールドをクリック(または Enter)し、次に一致すべきフィールドをクリックすると、両者の間にコネクターが描かれ、自動チェックでは対応付けられなかったもの(異なるキー、判断を要するケース)にフラグを立てられます。サイドパネルには、すべての不一致が各文書の値とともに一覧表示され、該当箇所へジャンプでき、メモを添えて受け入れまたは却下として解決できます。未解決または却下の項目がある間は承認できず、拒否の場合は理由が集められ、判定はキャンセル可能な mv-verdict イベントで送出されます。作成者の文書が移動されたり書き換えられたりすることはなく、フィールドや入力が変わると再チェックがリアルタイムで実行され、レイアウトは固定パネル付きの横並びから縦積みへと適応します。
| カテゴリー | データ表示 |
|---|---|
| タイプ | Web Component(<mv-cross-check>) |
| ステータス | 安定版 |
| 同時にインストールされるもの | button, input, textarea |
| Keywords | exclusive, culture, reconciliation, compare, cross-check, kyc, review, audit, three-way-match, invoice, purchase-order, discrepancy, verification, compliance, side-by-side, annotation, connector, approval, document |
When to use
- A compliance analyst must confirm that an application form, an ID and a proof of address describe the same person
- Accounts payable matches an invoice against its purchase order and delivery note before releasing payment
- A reviewer checks a signed contract against the quote, or a submitted form against the uploaded document
- Mismatches need a traceable decision: each one accepted or rejected with a note, then an approve or deny verdict
Avoid when
- The goal is to show what changed between two versions of the same text or image → use Image Compare instead
- A single form must be validated against rules, with no second document to compare it with → use Field instead
- Hundreds of records must be reconciled in bulk; this is a one-case review desk, use a data table with a match column → use Data table instead
インストール
node scripts/add.mjs cross-check --out ./src/marvelousMarvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["cross-check"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, components/button/button.css, components/input/input.css, core/dom.js, core/element.js, components/textarea/textarea.css, components/textarea/char-count.js, core/motion.js, core/observe.js, components/cross-check/cross-check.js, components/cross-check/cross-check.css。
使い方
基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<div id="xc-demo" style="width:min(100%,80rem);margin-inline:auto">
<style>
#xc-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1rem }
#xc-demo .xc-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap }
#xc-demo .xc-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#xc-demo .xc-top p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#xc-demo .xc-doc { display:grid; align-content:start; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs); overflow:hidden }
#xc-demo .xc-doc-head { display:flex; align-items:center; gap:.75rem; padding:.875rem 1rem; border-bottom:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#xc-demo .xc-doc-ico { display:grid; place-items:center; flex:none; width:2rem; height:2rem; border-radius:var(--mv-radius-md); background:var(--mv-surface); border:1px solid var(--mv-border); color:var(--mv-fg-muted) }
#xc-demo .xc-doc-ico svg { width:1rem; height:1rem }
#xc-demo .xc-doc-head h4 { margin:0; font-size:.875rem; font-weight:600; letter-spacing:-.005em }
#xc-demo .xc-doc-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.75rem }
#xc-demo .xc-fields { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.875rem; margin:0; padding:1rem }
#xc-demo .xc-fields > div { display:grid; gap:.1875rem; grid-column:1 / -1 }
#xc-demo .xc-fields > [data-half] { grid-column:auto }
#xc-demo .xc-fields dt { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:500; letter-spacing:.04em; text-transform:uppercase }
#xc-demo .xc-fields dd { justify-self:start; margin:0; font-size:.875rem; font-weight:500; font-variant-numeric:tabular-nums; line-height:1.35 }
#xc-demo .xc-total { margin:0 1rem 1rem; padding-top:.75rem; border-top:1px dashed var(--mv-border-strong); display:flex; justify-content:space-between; align-items:baseline; font-size:.8125rem; color:var(--mv-fg-muted) }
#xc-demo .xc-total strong { color:var(--mv-fg); font-size:.9375rem; font-variant-numeric:tabular-nums }
#xc-demo .xc-controls { display:flex; align-items:center; gap:.75rem 1.5rem; flex-wrap:wrap; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#xc-demo .xc-controls .mv-choice { font-size:.8125rem }
#xc-demo .xc-log { flex:1 1 12rem; min-width:0; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
@media (max-width:40rem) { #xc-demo .xc-log { text-align:start } }
@media (max-width:22rem) { #xc-demo .xc-fields > [data-half] { grid-column:1 / -1 } }
</style>
<div class="xc-top">
<div>
<h3 id="xc-title">Identity check · Case KYC-20931</h3>
<p id="xc-sub">Amélie Durand-Lefèvre · personal account · submitted Sep 18, 2026</p>
</div>
<mv-segmented id="xc-scenario" value="kyc" label="Review scenario">
<button value="kyc">Identity check</button>
<button value="ap">Three-way match</button>
</mv-segmented>
</div>
<!-- KYC: application form vs passport vs proof of address -->
<mv-cross-check id="xc-kyc" reference="application" label="Identity check for Amélie Durand-Lefèvre">
<article class="xc-doc" data-doc="application" data-doc-label="Application">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></svg></span>
<div><h4>Account application</h4><p>Online form · Sep 18, 2026</p></div>
</header>
<dl class="xc-fields">
<div><dt>Full name</dt><dd data-field="name">Amélie Durand-Lefèvre</dd></div>
<div data-half><dt>Date of birth</dt><dd data-field="dob" data-compare="date">03/14/1991</dd></div>
<div data-half><dt>Nationality</dt><dd data-field="nationality">French</dd></div>
<div><dt>ID document no.</dt><dd data-field="doc_number" data-compare="alnum">19AB 45872</dd></div>
<div><dt>Home address</dt><dd data-field="address">48 Rue de la Paix, 75002 Paris</dd></div>
<div><dt>Occupation</dt><dd data-field="occupation">Product designer</dd></div>
</dl>
</article>
<article class="xc-doc" data-doc="passport" data-doc-label="Passport">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="3" width="14" height="18" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M9 16.5h6"/></svg></span>
<div><h4>Passport</h4><p>Scanned · verified chip</p></div>
</header>
<dl class="xc-fields">
<div><dt>Holder</dt><dd data-field="name">AMELIE DURAND-LEFEVRE</dd></div>
<div data-half><dt>Date of birth</dt><dd data-field="dob">14 MAR 1991</dd></div>
<div data-half><dt>Nationality</dt><dd data-field="nationality">FRENCH</dd></div>
<div><dt>Document no.</dt><dd data-field="doc_number">19AB45827</dd></div>
<div data-half><dt>Place of birth</dt><dd data-field="birthplace">Lyon</dd></div>
<div data-half><dt>Expires</dt><dd data-field="expiry">11 JUL 2031</dd></div>
</dl>
</article>
<article class="xc-doc" data-doc="bill" data-doc-label="Utility bill">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 9.5V20h13V9.5"/><path d="M10 20v-5h4v5"/></svg></span>
<div><h4>Proof of address</h4><p>Electricity bill · Aug 29, 2026</p></div>
</header>
<dl class="xc-fields">
<div><dt>Account holder</dt><dd data-field="name">Amelie Durand</dd></div>
<div><dt>Service address</dt><dd data-field="service_address">12 Rue Oberkampf, 75011 Paris</dd></div>
<div data-half><dt>Billing period</dt><dd data-field="period">Jul 1 to Jul 31, 2026</dd></div>
<div data-half><dt>Customer no.</dt><dd data-field="customer_no">40-2281-7736</dd></div>
</dl>
<p class="xc-total"><span>Amount due</span><strong data-field="bill_amount" data-field-label="Amount due">$96.40</strong></p>
</article>
</mv-cross-check>
<!-- Accounts payable: purchase order vs invoice vs delivery note (rules set from script) -->
<mv-cross-check id="xc-ap" reference="po" label="Three-way match for invoice INV-20931" hidden>
<article class="xc-doc" data-doc="po" data-doc-label="Purchase order">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12v18l-3-2-3 2-3-2-3 2z"/><path d="M9 8h6M9 12h6"/></svg></span>
<div><h4>Purchase order</h4><p>Harbor & Pine Interiors · Sep 2, 2026</p></div>
</header>
<dl class="xc-fields">
<div><dt>Supplier</dt><dd data-field="supplier">Nordvik Timber AB</dd></div>
<div><dt>PO number</dt><dd data-field="po">PO-4471</dd></div>
<div><dt>Item</dt><dd data-field="item">Oak flooring, 14 mm</dd></div>
<div data-half><dt>Quantity</dt><dd data-field="qty">50 m²</dd></div>
<div data-half><dt>Unit price</dt><dd data-field="unit_price">$248.00</dd></div>
</dl>
<p class="xc-total"><span>Order total</span><strong data-field="total" data-field-label="Total">$12,400.00</strong></p>
</article>
<article class="xc-doc" data-doc="invoice" data-doc-label="Invoice">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M12 11v6M10 12.5c0-.8.9-1.5 2-1.5s2 .7 2 1.5-1 1.2-2 1.5-2 .7-2 1.5.9 1.5 2 1.5 2-.7 2-1.5"/></svg></span>
<div><h4>Invoice INV-20931</h4><p>Received Sep 23, 2026 · due Oct 23</p></div>
</header>
<dl class="xc-fields">
<div><dt>Supplier</dt><dd data-field="supplier">NORDVIK TIMBER AB</dd></div>
<div><dt>PO reference</dt><dd data-field="po">PO 4471</dd></div>
<div><dt>Item</dt><dd data-field="item">Oak flooring, 14 mm</dd></div>
<div data-half><dt>Quantity</dt><dd data-field="qty">50 m²</dd></div>
<div data-half><dt>Unit price</dt><dd data-field="unit_price">$248.95</dd></div>
</dl>
<p class="xc-total"><span>Amount due</span><strong data-field="total" data-field-label="Total">$12,447.50</strong></p>
</article>
<article class="xc-doc" data-doc="delivery" data-doc-label="Delivery note">
<header class="xc-doc-head">
<span class="xc-doc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h11v9H3zM14 10h4l3 3v3h-7z"/><circle cx="7" cy="17.5" r="1.8"/><circle cx="17" cy="17.5" r="1.8"/></svg></span>
<div><h4>Delivery note DN-883</h4><p>Signed on receipt · Sep 22, 2026</p></div>
</header>
<dl class="xc-fields">
<div><dt>Supplier</dt><dd data-field="supplier">Nordvik Timber AB</dd></div>
<div><dt>PO number</dt><dd data-field="po">PO-4471</dd></div>
<div><dt>Item</dt><dd data-field="item">Oak flooring, 14 mm</dd></div>
<div data-half><dt>Delivered</dt><dd data-field="qty">48 m²</dd></div>
<div data-half><dt>Received by</dt><dd data-field="received_by">Kwame Okafor</dd></div>
</dl>
<p class="xc-total"><span>Pallets</span><strong data-field="pallets" data-field-label="Pallets">6 of 6</strong></p>
</article>
</mv-cross-check>
<div class="xc-controls">
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="xc-all"> Show every connector</label>
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="xc-open"> Allow approval with open items</label>
<p class="xc-log" id="xc-log">Auto-check done. Turn on Inspect to flag a field pair.</p>
</div>
<script type="module">
const kyc = document.getElementById("xc-kyc");
const ap = document.getElementById("xc-ap");
const log = document.getElementById("xc-log");
const title = document.getElementById("xc-title");
const sub = document.getElementById("xc-sub");
const say = (text) => { log.textContent = `${new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" })} · ${text}`; };
// Accounts payable tolerances: unit price may drift 0.5 %, totals to the cent.
ap.rules = { qty: "number", unit_price: "number:0.5%", total: "number:0.01", po: "alnum" };
customElements.whenDefined("mv-cross-check").then(() => {
// The reviewer already linked the two addresses the auto-check can't pair (different keys)…
kyc.flag('[data-doc="application"] [data-field="address"]', '[data-doc="bill"] [data-field="service_address"]');
// …and accepted the shorter surname on the utility account.
kyc.resolve("auto:name", "accepted", { note: "Account opened before marriage; surname matches the passport." });
});
for (const el of [kyc, ap]) {
el.addEventListener("mv-inspect", (e) => say(e.detail.inspect ? "Inspect on: select two fields." : "Inspect off."));
el.addEventListener("mv-flag", (e) => say(`mv-flag · ${e.detail.fields.map((f) => `${f.docLabel} ${f.label}`).join(" ↔ ")}${e.detail.same ? " (identical)" : ""}`));
el.addEventListener("mv-resolve", (e) => say(`mv-resolve · #${e.detail.discrepancy.n} ${e.detail.discrepancy.label} → ${e.detail.status}`));
el.addEventListener("mv-verdict", (e) => say(`mv-verdict · ${e.detail.verdict} (${e.detail.reasons.length} reasons, ${e.detail.exceptions.length} exceptions)`));
el.addEventListener("mv-reopen", () => say("mv-reopen · review reopened"));
}
const scenarios = {
kyc: ["Identity check · Case KYC-20931", "Amélie Durand-Lefèvre · personal account · submitted Sep 18, 2026"],
ap: ["Three-way match · Invoice INV-20931", "Nordvik Timber AB · $12,447.50 · awaiting approval"],
};
document.getElementById("xc-scenario").addEventListener("mv-change", (e) => {
const v = e.detail.value;
kyc.hidden = v !== "kyc";
ap.hidden = v !== "ap";
[title.textContent, sub.textContent] = scenarios[v];
say(v === "ap" ? "Unit price differs by 0.38 %: within tolerance, so it matches." : "Auto-check done. Turn on Inspect to flag a field pair.");
});
document.getElementById("xc-all").addEventListener("change", (e) => {
for (const el of [kyc, ap]) el.connectors = e.target.checked ? "all" : "auto";
});
document.getElementById("xc-open").addEventListener("change", (e) => {
for (const el of [kyc, ap]) el.allowOpen = e.target.checked;
});
</script>
</div>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
reference | string (a data-doc value) | Document every other one is compared against (the purchase order, the ID…). Its field is the anchor of each connector. Defaults to the first document that has the key. | |
rule | exact | fold | alnum | number[:tol] | date[:mdy|dmy] | ignore | fold | Default rule for keys without a rule of their own. fold ignores case, accents and punctuation ("Amélie Durand-Lefèvre" = "AMELIE DURAND LEFEVRE"); alnum keeps letters and digits only (IBANs, document numbers); number:0.05 is an absolute tolerance, number:0.5% a relative one; date compares calendar days across formats (slashes read month first unless date:dmy, dots read day first); ignore never auto-compares (fields stay linkable in Inspect). Values that cannot be parsed as numbers or dates fall back to fold. Two empty values match; one empty value never does. |
inspect | boolean | Inspect mode: each field gets a target (a real toggle button overlaid on it); select one field, then another, to flag a discrepancy between them. Reflected by the bar's Inspect toggle. Ignored once a verdict is recorded. | |
connectors | auto | all | active | none | auto | Which connectors are drawn: auto shows reviewer flags always and an auto-check discrepancy while it is hovered, focused or selected; all shows every one; active only the current one; none never. |
marks | all | mismatch | none | all | In-place marks on the author's fields: all underlines matching fields too, mismatch only highlights discrepancies, none leaves the documents untouched visually (pips and the panel remain). |
allow-open | boolean | Allows approving while discrepancies are still open (they are reported as unresolved in mv-verdict). Rejected discrepancies always block approval. | |
label | string | Document review | Accessible name of the group (role="group"). |
locale | BCP 47 tag | en-US | Locale of the decision timestamp. |
data-doc / data-doc-label | on direct children | Marks each document and gives its id (used by reference and in events) and display name (else aria-label, the first heading, then “Document N”). Without any data-doc, every direct child is a document. | |
data-field | on any element inside a document | The field key shared across documents ("name", "dob", "total"…). Inputs, selects and textareas are read by value and re-checked as the user types. | |
data-value / data-field-label / data-compare | on a field | data-value: canonical value compared instead of the text (a formatted date, a rounded amount). data-field-label: human name (else aria-label, <label>, the preceding <dt>, the row's <th>, then the humanized key). data-compare: rule for this key when the rules property does not set one. | |
data-cross-check-mark / data-cross-check-active | set on fields by the component | match | mismatch | flag, and the current highlight. Style them to adapt the marks to your documents. | |
data-fit / data-inspecting / data-verdict | set on the host | wide (documents side by side + sticky panel) | split (documents side by side, panel below) | stack; present while inspecting; approved | denied once decided. |
Properties
| Name | タイプ | Description |
|---|---|---|
rules | Record<string, string | (a: string, b: string, ctx: { key, fields }) => boolean> | Rule per key, taking precedence over data-compare and the rule attribute: { total: "number:0.01", unit_price: "number:0.5%", dob: "date", iban: "alnum", policy: (a, b) => … }. Setting it re-runs the comparison. |
discrepancies | Array<{ id, n, kind: "auto" | "manual", key, label, status: "open" | "accepted" | "rejected", note, rule, same, fields: [{ element, key, label, value, text, doc, docLabel, flagged }] }> | Every discrepancy in list order (read-only snapshots). Auto-check ids are "auto:<key>"; reviewer flags are "flag-<n>". same is true when a reviewer flag links two values the rule considers equal. |
stats | { compared, matched, differ, flagged, open, accepted, rejected } | Keys compared, keys that agree, auto-check and reviewer discrepancies, and resolution counts (read-only). |
verdict | { verdict: "approve" | "deny", at: Date, note, reasons, exceptions } | null | The recorded decision, or null while under review (read-only). |
strings | Partial<Record<string, string | [one, other]>> | Overrides for every visible text and announcement (English defaults), e.g. inspect, hintIdle, title, empty, accept, reject, approve, deny, blockedOpen, approved, denied, reopen, announceFlagged. Pairs are chosen with Intl.PluralRules. |
inspect / reference / rule / connectors / marks / allowOpen / label / locale | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
compare() | Re-scans the documents and re-runs every rule now (it also happens automatically on DOM and input changes). Returns stats. |
flag(a, b, { note? }) | Flags a discrepancy between two fields (elements or selectors inside the component), exactly like two picks in Inspect. Returns its id, the id of the existing discrepancy when the pair is already listed, or null (cancelled, unknown field, decided). |
unflag(id) | Removes a reviewer flag. Auto-check discrepancies cannot be removed, only resolved. |
resolve(id, status, { note? }) | status: "open" | "accepted" | "rejected". Emits the cancelable mv-resolve. Returns false if cancelled or decided. |
select(id, { scroll? }) | Highlights a discrepancy (fields, connector, list item) and scrolls its first field into view with a brief halo. |
approve({ note? }) / deny({ note? }) | Records the verdict (the note defaults to the decision field). approve fails while discrepancies are open (unless allow-open) or rejected; deny needs a rejected or open discrepancy, or a note. Emits the cancelable mv-verdict; returns true once recorded. |
reopen() | Clears the verdict and makes everything editable again. Emits mv-reopen. |
Events
| Name | Description |
|---|---|
mv-compare | The auto-check result changed (first run included). detail: { compared, matched, differ, flagged, open, accepted, rejected, discrepancies }. |
mv-flag | Cancelable, before a reviewer flag is added (Inspect or flag()). detail: { source: "inspect" | "api", same, fields: [a, b] } with each field { element, key, label, value, text, doc, docLabel }. preventDefault() refuses it. |
mv-unflag | A reviewer flag was removed. detail: { discrepancy }. |
mv-resolve | Cancelable, before a discrepancy changes status. detail: { discrepancy, status, previous, note }. |
mv-note | The note of a discrepancy was edited (on change). detail: { discrepancy, note }. |
mv-inspect | Inspect mode turned on or off. detail: { inspect }. |
mv-verdict | Cancelable, before the decision is recorded. detail: { verdict: "approve" | "deny", note, reasons (rejected discrepancies, plus open ones when denying), exceptions (accepted ones), discrepancies, stats }. Send it to your case system; preventDefault() keeps the review open (e.g. a second approver is required). |
mv-reopen | The verdict was cleared. detail: { previous }. |
Content structure
| Name | Description |
|---|---|
(content) | Two or more documents as direct children (article, section, div, a form…), each with data-doc and fields marked data-field. They stay where they are; the component prepends its bar and appends an overlay and the discrepancy panel as siblings, laid out with CSS grid. |
CSS classes
| Name | Description |
|---|---|
mv-cross-check-bar | Top bar: .mv-cross-check-inspect (aria-pressed toggle), .mv-cross-check-hint, .mv-cross-check-tally with .mv-cross-check-chip[data-tone=match|mismatch|flag]. |
mv-cross-check-overlay | aria-hidden decorations and Inspect targets over the documents: .mv-cross-check-links (SVG: .mv-cross-check-link-group[data-kind][data-status][data-active] > .mv-cross-check-link, .mv-cross-check-link-end; .mv-cross-check-band while picking), .mv-cross-check-pip (numbered marker), .mv-cross-check-halo, .mv-cross-check-hits > .mv-cross-check-hit buttons. |
mv-cross-check-panel | Discrepancy panel: -title, -count, -summary, .mv-cross-check-list > .mv-cross-check-item[data-kind][data-status][data-active] (.mv-cross-check-item-main, -num, -item-label, -status, -values > .mv-cross-check-value[data-diff][data-ref], -meta, .mv-cross-check-resolve with -accept / -reject / -remove, .mv-cross-check-note), -empty. |
mv-cross-check-verdict | Decision footer: .mv-cross-check-decide (decision note, .mv-cross-check-block reason, .mv-cross-check-deny / .mv-cross-check-approve) and .mv-cross-check-result[data-verdict] (icon, title, time, reasons, .mv-cross-check-reopen). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-cross-check-match | var(--mv-success) | Matching fields, accepted discrepancies, approved verdict. |
--mv-cross-check-mismatch | var(--mv-danger) | Auto-check mismatches, rejected discrepancies, denied verdict. |
--mv-cross-check-flag | var(--mv-warning) | Reviewer flags (fields, pips, connectors). |
--mv-cross-check-link | var(--mv-accent) | Inspect mode: targets, picked field, rubber band, halos. |
--mv-cross-check-panel-width | 20rem | Width of the panel column in the wide layout. |
--mv-cross-check-gap | var(--mv-space-4) | Gap between documents, bar and panel. |
--mv-cross-check-sticky-top | var(--mv-space-4) | Offset of the sticky panel (for a fixed header). |
Accessibility
The host is a labelled group; the author's documents keep their own semantics (the component never adds roles to them, only data attributes and aria-describedby). Every flagged field is described by a hidden sentence (“Discrepancy 2, ID document no.: Application 19AB 45872; Passport 19AB45827. Open.”) and every discrepancy is a real button in an ordered list labelled by the panel title and described by the visible summary (“2 open · 1 accepted”), so each flag is reachable and fully readable by keyboard without the visuals. Activating it highlights and scrolls to its fields; Accept and Reject are aria-pressed toggles in a group named after the discrepancy, followed by a labelled note field. Inspect is an aria-pressed toggle; in Inspect mode the fields become one roving tab stop of toggle buttons (“Holder, Passport: AMELIE DURAND-LEFEVRE”, pressed once picked): Up/Down move within a document, Left/Right jump to the same key (or the nearest field) in the next document, Home/End (Ctrl for all documents), Enter or Space picks then links, Escape cancels the pick then leaves Inspect with focus back on the toggle. A single polite live region announces each action with the remaining count (“Flagged Home address / Service address: Application and Utility bill. 2 open discrepancies.”), live re-checks (“Date of birth now matches.”), and the verdict; nothing is announced on load. Connectors, pips and halos are aria-hidden with pointer-events: none. Approve stays focusable with aria-disabled and an aria-describedby reason while it is blocked; after a decision focus moves to the result, and Reopen returns it to Approve. Colors always come with words or shape (numbered pips, dashed vs solid connectors, status pills). Reduced motion (OS or data-motion="reduce"): no connector draw-in, no marching rubber band, no halo pulse, instant scrolling. Forced colors: marks become Highlight outlines (dashed for reviewer flags), connectors use CanvasText.