익스클루시브
Cross-Check <mv-cross-check>
검토 워크플로(KYC, 송장과 구매 주문서와 납품서 대조, 계약서와 견적서 대조, 양식 제출과 신분증 대조)를 위한 문서 간 대조 데스크입니다.
문화적 레퍼런스
페이퍼스, 플리즈, 루카스 포프 (2013, 게임). 국경 검문소에서 심사관은 한 서류의 세부 항목을 클릭한 다음 다른 서류의 대응 항목을 클릭해 불일치를 드러내고, 증거를 바탕으로 입국자를 한 명씩 승인하거나 거부해야 합니다. 여기서는 어떤 서류 묶음이든 필드 단위로 교차 검증하고 불일치를 제자리에 표시합니다. 검토자는 임의의 두 필드를 연결해 검사가 놓친 부분을 지적한 뒤, 각 불일치를 해결하고 사유와 함께 승인 또는 거부 판정을 기록합니다.
작동 방식
검토 워크플로(KYC, 송장 대 구매 주문서 대 납품서, 계약서 대 견적서, 양식 제출 대 신분증)를 위한 서류 간 대조 데스크입니다. 두 개 이상의 서류를 나란히 배치하면(필드에 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.