Exclusivobeta
Papers <mv-papers>
Mesa de conciliación entre documentos para flujos de revisión (KYC, factura frente a orden de compra frente a nota de entrega, contrato frente a presupuesto, formulario enviado frente a documento de identidad). Coloca dos o más documentos uno al lado de otro (cualquier marcado cuyos campos lleven data-field="key"): cada clave presente en varios documentos se compara con un documento de referencia según una regla por clave (exacta, sin distinguir mayúsculas ni acentos mediante fold, solo letras y dígitos, tolerancia numérica absoluta o en %, igualdad de fechas entre formatos como 03/14/1991 y 14 MAR 1991, o tu propia función) y se marca en su sitio, con un subrayado punteado para las coincidencias y un resaltado más un punto numerado para las discrepancias. El modo de inspección convierte cada campo en un objetivo: haz clic en un campo (o pulsa Enter sobre él) y luego en aquel con el que debería coincidir, y se dibuja un conector entre ambos para señalar lo que la comprobación automática no pudo emparejar (claves distintas, cuestiones de criterio). Un panel lateral lista cada discrepancia con los valores de cada documento, salta a ella y la resuelve como aceptada o rechazada con una nota; la aprobación queda bloqueada mientras haya elementos abiertos o rechazados, la denegación recoge los motivos, y la decisión sale mediante un evento mv-verdict cancelable. Los documentos del autor nunca se mueven ni se reescriben, las comprobaciones se repiten en vivo cuando cambian los campos o las entradas, y el layout se adapta desde la vista lado a lado con un panel fijo hasta la vista apilada.
| Categoría | Visualización de datos |
|---|---|
| Tipo | Web Component (<mv-papers>) |
| Estado | beta |
| También instala | 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
Instalación
node scripts/add.mjs papers --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["papers"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): 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.
Uso
Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:
<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 to 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>Referencia cultural
Papers, Please, Lucas Pope (2013, juego). En un puesto fronterizo, el inspector hace clic en un detalle de un documento y luego en el detalle correspondiente de otro para destapar una discrepancia, y debe aprobar o denegar a cada persona que llega a partir de las pruebas. Aquí cualquier conjunto de documentos se contrasta campo por campo, las discrepancias se marcan en su sitio, el revisor vincula dos campos cualesquiera para señalar lo que la comprobación pasó por alto y después resuelve cada discrepancia y registra un veredicto de aprobación o denegación con sus motivos.
API
Attributes
| Name | Tipo | 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 | Tipo | 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.