Exclusivebeta
Papers — <mv-papers>
Cross-document reconciliation desk for review workflows (KYC, invoice vs purchase order vs delivery note, contract vs quote, form submission vs ID). Lay two or more documents side by side (any markup whose fields carry data-field="key"): every key found in several documents is compared against a reference document with a per-key rule (exact, case- and accent-insensitive via fold, letters-and-digits only, numeric tolerance absolute or %, date equality across formats like 03/14/1991 and 14 MAR 1991, or your own function) and marked in place, with a dotted underline for matches and a highlight plus numbered pip for mismatches. Inspect mode turns every field into a target: click (or Enter on) one field, then the one it should agree with, and a connector is drawn between them to flag what the auto-check could not pair (different keys, judgement calls). A side panel lists every discrepancy with the values of each document, jumps to it, and resolves it as accepted or rejected with a note; approval is blocked while items are open or rejected, denial collects the reasons, and the decision goes out through a cancelable mv-verdict event. The author's documents are never moved or rewritten, re-checks run live when fields or inputs change, and the layout adapts from side-by-side with a sticky panel to stacked.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-papers>) |
| Status | beta |
| Also installs | 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
- 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
Install
node scripts/add.mjs papers --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["papers"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/papers/papers.js, components/papers/papers.css, components/button/button.css, components/input/input.css, components/textarea/textarea.css, components/textarea/char-count.js.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="pp-demo" style="width:min(100%,80rem);margin-inline:auto">
<style>
#pp-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1rem }
#pp-demo .pp-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap }
#pp-demo .pp-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#pp-demo .pp-top p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#pp-demo .pp-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 }
#pp-demo .pp-doc-head { display:flex; align-items:center; gap:.75rem; padding:.875rem 1rem; border-bottom:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
#pp-demo .pp-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) }
#pp-demo .pp-doc-ico svg { width:1rem; height:1rem }
#pp-demo .pp-doc-head h4 { margin:0; font-size:.875rem; font-weight:600; letter-spacing:-.005em }
#pp-demo .pp-doc-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.75rem }
#pp-demo .pp-fields { display:grid; gap:.875rem; margin:0; padding:1rem }
#pp-demo .pp-fields > div { display:grid; gap:.1875rem }
#pp-demo .pp-fields dt { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:500; letter-spacing:.04em; text-transform:uppercase }
#pp-demo .pp-fields dd { justify-self:start; margin:0; font-size:.875rem; font-weight:500; font-variant-numeric:tabular-nums; line-height:1.35 }
#pp-demo .pp-pair { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,7.5rem),1fr)); gap:.875rem }
#pp-demo .pp-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) }
#pp-demo .pp-total strong { color:var(--mv-fg); font-size:.9375rem; font-variant-numeric:tabular-nums }
#pp-demo .pp-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) }
#pp-demo .pp-controls .mv-choice { font-size:.8125rem }
#pp-demo .pp-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) { #pp-demo .pp-log { text-align:start } }
</style>
<div class="pp-top">
<div>
<h3 id="pp-title">Identity check · Case KYC-20931</h3>
<p id="pp-sub">Amélie Durand-Lefèvre · personal account · submitted Sep 18, 2026</p>
</div>
<mv-segmented id="pp-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-papers id="pp-kyc" reference="application" label="Identity check for Amélie Durand-Lefèvre">
<article class="pp-doc" data-doc="application" data-doc-label="Application">
<header class="pp-doc-head">
<span class="pp-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="pp-fields">
<div><dt>Full name</dt><dd data-field="name">Amélie Durand-Lefèvre</dd></div>
<div class="pp-pair">
<div><dt>Date of birth</dt><dd data-field="dob" data-compare="date">03/14/1991</dd></div>
<div><dt>Nationality</dt><dd data-field="nationality">French</dd></div>
</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="pp-doc" data-doc="passport" data-doc-label="Passport">
<header class="pp-doc-head">
<span class="pp-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="pp-fields">
<div><dt>Holder</dt><dd data-field="name">AMELIE DURAND-LEFEVRE</dd></div>
<div class="pp-pair">
<div><dt>Date of birth</dt><dd data-field="dob">14 MAR 1991</dd></div>
<div><dt>Nationality</dt><dd data-field="nationality">FRENCH</dd></div>
</div>
<div><dt>Document no.</dt><dd data-field="doc_number">19AB45827</dd></div>
<div class="pp-pair">
<div><dt>Place of birth</dt><dd data-field="birthplace">Lyon</dd></div>
<div><dt>Expires</dt><dd data-field="expiry">11 JUL 2031</dd></div>
</div>
</dl>
</article>
<article class="pp-doc" data-doc="bill" data-doc-label="Utility bill">
<header class="pp-doc-head">
<span class="pp-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="pp-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 class="pp-pair">
<div><dt>Billing period</dt><dd data-field="period">Jul 1 – Jul 31, 2026</dd></div>
<div><dt>Customer no.</dt><dd data-field="customer_no">40-2281-7736</dd></div>
</div>
</dl>
<p class="pp-total"><span>Amount due</span><strong data-field="bill_amount" data-field-label="Amount due">$96.40</strong></p>
</article>
</mv-papers>
<!-- Accounts payable: purchase order vs invoice vs delivery note (rules set from script) -->
<mv-papers id="pp-ap" reference="po" label="Three-way match for invoice INV-20931" hidden>
<article class="pp-doc" data-doc="po" data-doc-label="Purchase order">
<header class="pp-doc-head">
<span class="pp-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="pp-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 class="pp-pair">
<div><dt>Quantity</dt><dd data-field="qty">50 m²</dd></div>
<div><dt>Unit price</dt><dd data-field="unit_price">$248.00</dd></div>
</div>
</dl>
<p class="pp-total"><span>Order total</span><strong data-field="total" data-field-label="Total">$12,400.00</strong></p>
</article>
<article class="pp-doc" data-doc="invoice" data-doc-label="Invoice">
<header class="pp-doc-head">
<span class="pp-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="pp-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 class="pp-pair">
<div><dt>Quantity</dt><dd data-field="qty">50 m²</dd></div>
<div><dt>Unit price</dt><dd data-field="unit_price">$248.95</dd></div>
</div>
</dl>
<p class="pp-total"><span>Amount due</span><strong data-field="total" data-field-label="Total">$12,447.50</strong></p>
</article>
<article class="pp-doc" data-doc="delivery" data-doc-label="Delivery note">
<header class="pp-doc-head">
<span class="pp-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="pp-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 class="pp-pair">
<div><dt>Delivered</dt><dd data-field="qty">48 m²</dd></div>
<div><dt>Received by</dt><dd data-field="received_by">Kwame Okafor</dd></div>
</div>
</dl>
<p class="pp-total"><span>Pallets</span><strong data-field="pallets" data-field-label="Pallets">6 of 6</strong></p>
</article>
</mv-papers>
<div class="pp-controls">
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="pp-all"> Show every connector</label>
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="pp-open"> Allow approval with open items</label>
<p class="pp-log" id="pp-log">Auto-check done. Turn on Inspect to flag a field pair.</p>
</div>
<script type="module">
const kyc = document.getElementById("pp-kyc");
const ap = document.getElementById("pp-ap");
const log = document.getElementById("pp-log");
const title = document.getElementById("pp-title");
const sub = document.getElementById("pp-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-papers").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("pp-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("pp-all").addEventListener("change", (e) => {
for (const el of [kyc, ap]) el.connectors = e.target.checked ? "all" : "auto";
});
document.getElementById("pp-open").addEventListener("change", (e) => {
for (const el of [kyc, ap]) el.allowOpen = e.target.checked;
});
</script>
</div>Cultural reference
Papers, Please — Lucas Pope (2013, game). At a border checkpoint, the inspector clicks a detail on one document, then the matching detail on another, to expose a discrepancy, and must approve or deny each entrant from the evidence. Here any set of documents is cross-checked field by field, mismatches are marked in place, the reviewer links any two fields to flag what the check missed, then resolves each discrepancy and records an approve or deny verdict with reasons.
API
Attributes
| Name | Type | 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-papers-mark / data-papers-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 | Type | 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-papers-bar | Top bar: .mv-papers-inspect (aria-pressed toggle), .mv-papers-hint, .mv-papers-tally with .mv-papers-chip[data-tone=match|mismatch|flag]. |
mv-papers-overlay | aria-hidden decorations and Inspect targets over the documents: .mv-papers-links (SVG: .mv-papers-link-group[data-kind][data-status][data-active] > .mv-papers-link, .mv-papers-link-end; .mv-papers-band while picking), .mv-papers-pip (numbered marker), .mv-papers-halo, .mv-papers-hits > .mv-papers-hit buttons. |
mv-papers-panel | Discrepancy panel: -title, -count, -summary, .mv-papers-list > .mv-papers-item[data-kind][data-status][data-active] (.mv-papers-item-main, -num, -item-label, -status, -values > .mv-papers-value[data-diff][data-ref], -meta, .mv-papers-resolve with -accept / -reject / -remove, .mv-papers-note), -empty. |
mv-papers-verdict | Decision footer: .mv-papers-decide (decision note, .mv-papers-block reason, .mv-papers-deny / .mv-papers-approve) and .mv-papers-result[data-verdict] (icon, title, time, reasons, .mv-papers-reopen). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-papers-match | var(--mv-success) | Matching fields, accepted discrepancies, approved verdict. |
--mv-papers-mismatch | var(--mv-danger) | Auto-check mismatches, rejected discrepancies, denied verdict. |
--mv-papers-flag | var(--mv-warning) | Reviewer flags (fields, pips, connectors). |
--mv-papers-link | var(--mv-accent) | Inspect mode: targets, picked field, rubber band, halos. |
--mv-papers-panel-width | 20rem | Width of the panel column in the wide layout. |
--mv-papers-gap | var(--mv-space-4) | Gap between documents, bar and panel. |
--mv-papers-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.