エクスクルーシブ
Accounts <mv-accounts>
同じ出来事についての複数の証言を並べ、事実ごとに一致している点、食い違っている点、ある情報源が沈黙している点を示すマルチソースのタイムラインです。用途:インシデントのポストモーテム(ゲートウェイのログ、決済プロバイダー、クライアントのテレメトリー、オンコールのメモの比較)、配送トラブル(顧客、倉庫、運送会社)、監査証跡、ファクトチェック、複数の参加者による議事録。
文化的リファレンス
羅生門、黒澤明(1950年、映画)。ひとつの出来事が4人の証人によって4通りに語られます。どの証言ももっともらしく、互いに矛盾しているため、真実は突き合わせて推し量るしかありません。UI では、ある出来事についての各ソースの証言がそれぞれレーンになり、同じ事実がレーンをまたいで並べられ、一致・矛盾・裏付けなしとして判定されます。物語は横に並べて読むことも、統合して読むことも、ひとりの証人の語りとして読むこともできます。
仕組み
同じ出来事についての複数の証言を並べ、事実ごとに、どこで一致し、どこで矛盾し、どこで情報源が沈黙しているかを示すマルチソースのタイムライン:インシデントのポストモーテム(ゲートウェイのログ、決済プロバイダー、クライアントのテレメトリ、オンコールのメモ)、配送トラブル(顧客、倉庫、運送業者)、監査証跡、ファクトチェック、複数の参加者による会議メモなどに使えます。各情報源はシンプルな <ol data-source>(または sources プロパティ)で、タイムスタンプ付きの <li data-at data-key> エントリーが事実を主張します。同じキーを持つエントリーは 1 行にそろえられて判定されます。一致、矛盾(異なる data-value、または許容範囲外の時刻。多数派は事実ごとに計算され、同数の場合は任意の参照情報源で決着します)、または単一の情報源のみです。ある行に含まれない情報源は沈黙として表示されます。ビューは 3 つです。Side by side(情報源ごとに 1 レーン。行は事実ごとにそろえられ、レーンをまたぐリンクと合意時刻のガターが描かれます)、Merged(各事実について、裏付ける情報源、矛盾する情報源、合意メーターを示す 1 本の年表)、One witness(1 つの情報源が語る順に並べ替えたストーリー。ほかの情報源は彩度を落として横に表示されます)。時計はずれるものなので、各情報源にはオフセットがあり、Clocks パネルで編集するか、ほかの情報源と共有する事実から推定できます(Match、Align all clocks)。行は FLIP で並べ替わり、判定が変わると点滅します。サマリー(「3 conflicts · 2 facts only one source mentions」)はすべての不一致をジャンプリンクとして一覧表示し、事実を選ぶとその行の下に文章での判定が開きます。すべてのステータスはテキストで明示され、固有の形で描かれ、色だけに頼ることはありません。
| カテゴリー | データ表示 |
|---|---|
| タイプ | Web Component(<mv-accounts>) |
| ステータス | 安定版 |
| Keywords | exclusive, culture, timeline, multi-source, reconciliation, post-mortem, incident, audit-trail, dispute, fact-check, comparison, consensus, conflict, clock-skew, swimlanes, logs, provenance, keyboard |
When to use
- An incident post-mortem must reconcile logs, vendor reports, client telemetry and human notes into one agreed timeline
- A support or claims team compares a customer's story with carrier and warehouse records to settle a dispute
- Several sources report the same events with clocks that drift, and the skew must be corrected before judging who is right
- Fact-checkers or auditors need to see at a glance which claims are corroborated, contradicted or backed by a single source
Avoid when
- The documents are static forms or records compared field by field rather than timestamped narratives → use Cross-Check instead
- There is a single source of events: a plain chronological list or activity feed is clearer → use Animated List instead
- Thousands of events must be reconciled in bulk; this is a review surface for tens of facts, use a data table with a match column → use Data table instead
インストール
node scripts/add.mjs accounts --out ./src/marvelousMarvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["accounts"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css、core/base.css、core/dom.js、core/element.js、core/focus.js、core/motion.js、components/accounts/accounts.js、components/accounts/accounts.css。
使い方
基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<div id="acc-demo" style="width:min(100%,78rem);margin-inline:auto">
<style>
#acc-demo { display:grid; gap:2.25rem }
#acc-demo .acc-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap; margin:0 0 .875rem }
#acc-demo .acc-kicker { display:flex; align-items:center; gap:.5rem; margin:0 0 .375rem; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
#acc-demo .acc-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#acc-demo .acc-sub { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:62ch }
#acc-demo .acc-side { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
#acc-demo .acc-side .mv-choice { font-size:.8125rem }
#acc-demo .acc-log { margin:.625rem 0 0; min-height:1.25rem; color:var(--mv-fg-muted); font-size:.75rem }
#acc-demo .acc-log b { color:var(--mv-fg); font-weight:600 }
#acc-demo code { font-family:var(--mv-font-mono); font-size:.92em }
</style>
<section aria-labelledby="acc-t1">
<div class="acc-head">
<div>
<p class="acc-kicker"><span class="mv-badge" data-variant="danger" data-shape="pill">SEV-2</span> INC-2291 · Sep 14, 2026 · post-mortem draft</p>
<h3 id="acc-t1">Checkout payments failing for 12 minutes</h3>
<p class="acc-sub">Four sources, one outage. Rows line up the same fact across every account; select one to read the verdict, or switch to Merged and One witness.</p>
</div>
<div class="acc-side">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="acc-skew" checked>
<span class="mv-choice-text"><span class="mv-choice-title">Correct the mobile app clock</span></span>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="acc-next">Next conflict</button>
</div>
</div>
<mv-accounts id="acc-incident" tolerance="1m" time-zone="UTC" label="Incident accounts" selected="degraded">
<ol data-source="API gateway" data-id="gateway" data-description="Edge access logs">
<li data-at="2026-09-14T14:01:05Z" data-key="deploy" data-label="Deploy v4.18.0">Config reload after deploy <code>v4.18.0</code></li>
<li data-at="2026-09-14T14:02:29Z" data-key="degraded" data-label="Payments start timing out">First upstream timeout to payments (30 s)</li>
<li data-at="2026-09-14T14:02:41Z" data-key="first-502" data-value="502" data-label="First 502 at checkout">on <code>POST /checkout</code></li>
<li data-at="2026-09-14T14:08:10Z" data-key="retries" data-label="Retry storm">Traffic on /checkout up 4×, mostly retries</li>
<li data-at="2026-09-14T14:12:20Z" data-key="rollback" data-value="v4.17.2" data-label="Rollback">Config reload after rollback</li>
<li data-at="2026-09-14T14:14:00Z" data-key="recovered" data-label="Checkout recovers">Error rate back under 0.5%</li>
</ol>
<ol data-source="Payment provider" data-id="provider" data-description="Webhook log and RCA">
<li data-at="2026-09-14T14:06:30Z" data-key="degraded">Incident start, per the provider’s RCA</li>
<li data-at="2026-09-14T14:13:30Z" data-key="charges" data-value="0 captured" data-label="Charges during the outage">No charges captured from 14:02 to 14:14</li>
<li data-at="2026-09-14T14:14:30Z" data-key="recovered">Status page: resolved</li>
</ol>
<ol data-source="Mobile app" data-id="app" data-offset="-2m40s" data-description="Client telemetry">
<li data-at="2026-09-14T14:05:11Z" data-key="degraded">Card payment spinner, then a timeout</li>
<li data-at="2026-09-14T14:05:24Z" data-key="first-502" data-value="502">“Something went wrong” shown to 1,204 users</li>
<li data-at="2026-09-14T14:10:53Z" data-key="retries">Auto-retry fires up to 3× per tap</li>
<li data-at="2026-09-14T14:16:05Z" data-key="charges" data-value="Charged twice">Support ticket: charged twice for order #88121</li>
<li data-at="2026-09-14T14:16:40Z" data-key="recovered">Checkout success rate back to 99%</li>
</ol>
<ol data-source="On-call notes" data-id="oncall" data-description="Incident channel">
<li data-at="2026-09-14T14:01:00Z" data-key="deploy">Maya Okafor ships v4.18.0</li>
<li data-at="2026-09-14T14:07:00Z" data-key="paged" data-label="On-call paged">Paged: checkout error rate at 6%</li>
<li data-at="2026-09-14T14:12:00Z" data-key="rollback" data-value="v4.17.2">Rolled back to v4.17.2</li>
<li data-at="2026-09-14T14:18:00Z" data-key="recovered">Declared resolved in #incidents</li>
<li data-at="2026-09-14T14:25:00Z" data-key="rca" data-label="Suspected cause">New connection-pool limit in v4.18.0</li>
</ol>
</mv-accounts>
<p class="acc-log" id="acc-log" aria-live="off">Tip: Alt + ←/→ jumps to the same fact in the next source, Alt + ↑/↓ to the next disagreement.</p>
</section>
<section aria-labelledby="acc-t2">
<div class="acc-head">
<div>
<p class="acc-kicker"><span class="mv-badge" data-variant="warning" data-shape="pill">Dispute</span> Order #88342 · Chicago time</p>
<h3 id="acc-t2">“My package never arrived”</h3>
<p class="acc-sub">Customer, warehouse and carrier merged into one chronology: each fact shows who backs it and who contradicts it. Weight readings are compared on value only.</p>
</div>
</div>
<mv-accounts id="acc-dispute" view="merged" tolerance="30m" time-zone="America/Chicago" label="Delivery dispute accounts">
<ol data-source="Customer" data-description="Support ticket #44710">
<li data-at="2026-09-08T09:12:00-05:00" data-key="ordered" data-label="Order placed">Placed order #88342 on the website</li>
<li data-at="2026-09-10T08:05:00-05:00" data-key="out" data-label="Out for delivery">Got the “out for delivery” text</li>
<li data-at="2026-09-10T18:30:00-05:00" data-key="delivered" data-value="Not received" data-label="Delivery">Nothing at the door when I got home</li>
</ol>
<ol data-source="Warehouse" data-description="Scan events">
<li data-at="2026-09-08T09:13:00-05:00" data-key="ordered">Order received by the warehouse system</li>
<li data-at="2026-09-08T14:40:00-05:00" data-key="packed" data-label="Packed">Packed and sealed, 2 items</li>
<li data-at="2026-09-08T14:41:00-05:00" data-key="weight" data-value="1.4 kg" data-tolerance="any" data-label="Parcel weight">Weighed at the packing station</li>
<li data-at="2026-09-08T17:05:00-05:00" data-key="handoff" data-label="Handed to carrier">Handed over, manifest 7731</li>
</ol>
<ol data-source="Carrier" data-description="Tracking history">
<li data-at="2026-09-08T17:20:00-05:00" data-key="handoff">Picked up at origin</li>
<li data-at="2026-09-09T03:10:00-05:00" data-key="weight" data-value="0.6 kg" data-tolerance="any">Re-weighed at the regional hub</li>
<li data-at="2026-09-10T08:02:00-05:00" data-key="out">Out for delivery</li>
<li data-at="2026-09-10T11:47:00-05:00" data-key="delivered" data-value="Delivered">Left at front door, photo on file</li>
</ol>
</mv-accounts>
</section>
<script type="module">
const inc = document.getElementById("acc-incident");
await customElements.whenDefined("mv-accounts");
const log = document.getElementById("acc-log");
// Toggle the known clock drift of the mobile app: uncorrected, its skew fakes conflicts.
document.getElementById("acc-skew").addEventListener("change", (e) => {
inc.setOffset("app", e.target.checked ? "-2m40s" : 0);
});
inc.addEventListener("mv-offset-change", (e) => {
if (e.detail.source === "app") document.getElementById("acc-skew").checked = e.detail.offset !== 0;
});
document.getElementById("acc-next").addEventListener("click", () => {
const issues = inc.facts.filter((f) => f.status === "conflict");
if (!issues.length) return;
const i = issues.findIndex((f) => f.key === inc.selected);
inc.select(issues[(i + 1) % issues.length].key, { focus: true });
});
inc.addEventListener("mv-select-fact", (e) => {
const { fact } = e.detail;
log.replaceChildren("mv-select-fact → ", Object.assign(document.createElement("b"), { textContent: fact.label }), ` · ${fact.status} · ${fact.entries.length} of ${inc.sources.length} sources`);
});
inc.addEventListener("mv-view-change", (e) => {
log.replaceChildren("mv-view-change → ", Object.assign(document.createElement("b"), { textContent: e.detail.view }));
});
</script>
</div>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
view | lanes | merged | witness | lanes | lanes: one column per source, rows aligned by fact. merged: one chronology of facts with their supporters, dissenters and a consensus meter. witness: lanes reordered by the witness source's own chronology, that lane first and wider, the others dimmed. |
witness | source id | Source focused by the witness view (default: the first source). | |
tolerance | duration | 1m | Times closer than this count as the same moment ("90s", "2m", "1h", "500ms" or ms). Per fact, data-tolerance on any entry (or factLabels[key].tolerance) overrides it; "any" means time is not compared for that fact (e.g. a measurement taken twice). |
reference | source id | System of record: wins value and time ties when no majority exists, and is the clock kept fixed by Align all clocks (default anchor: the first source). | |
selected | fact key | Selected fact: highlighted in every view, with a written verdict under its row in the lanes views. Reflects user selection. | |
filter | all | issues | all | issues hides facts on which every source agrees (the “Only disagreements” toggle). |
locale | BCP 47 | en-US | Locale for times, durations, plurals and name lists. |
time-zone | IANA zone | Zone used to display times (default: the viewer's; UTC when every time is wall-clock only). Shown in the gutter header. | |
precision | auto | seconds | minutes | auto | Time display precision; auto shows seconds when any entry has them. |
label | string | Accounts | Accessible name of the timeline group. |
heading-level | 1-6 | 3 | Level of the lane headings. |
controls | "true" | "false" | true | "false" hides the built-in toolbar (views, filter, Clocks) when the app drives them. |
data-source | string | On a child <ol>/<ul>: the source's display name. Makes it a lane. The list itself is hidden and re-read live when it changes. | |
data-id / data-offset / data-description | string / duration / string | On a source list: stable id (default: slug of the name), clock correction added to its times ("-2m40s" for a clock 2 min 40 s fast) and a short description under its heading. | |
data-at | ISO date-time | epoch ms | HH:MM[:SS] | On an entry: when the source says it happened. Wall-clock times are compared as is. Entries without a time are ordered after the previous timed entry of their source and never conflict on time. | |
data-key | string | On an entry: the fact it claims. Same key across sources = same fact. Entries without a key are facts only their source mentions. | |
data-value | string | On an entry: the comparable claim (status code, amount, outcome). Compared case-, accent- and space-insensitively (or with the compare property); entries without a value never conflict on value. | |
data-label / data-tolerance | string / duration | "any" | On an entry: the fact's title (first one found wins, default: the key humanized) and a per-fact tolerance. |
Properties
| Name | タイプ | Description |
|---|---|---|
sources | Array<{ id?, name, offset?, description?, entries: Array<{ key?, at?, value?, text, label?, tolerance? }> }> | Data alternative to the markup (takes precedence over child lists). Reading it returns the current model with live offsets. |
facts | Array<Fact> | Read-only analysis in consensus order: { key, label, status: "agree" | "conflict" | "single", at (consensus ms), conflict: { value, time }, value (majority), entries: [{ source, key, at, reportedAt, value, text, status: "agree" | "dissent" | "split" | "single", delta, node }], silent: [source ids], verdict (sentence) }. |
summary | { facts, agreed, conflicts, single, silentGaps } | Counts behind the summary line (read-only). |
offsets | Record<sourceId, ms> | Current clock corrections (read-only; use setOffset). |
factLabels | Record<key, { label?, tolerance? }> | Titles and tolerances per fact, when the markup cannot carry them. |
compare | (a: string, b: string, key: string) => boolean | Custom value equality (numeric tolerance, synonyms…). Default: case-, accent- and whitespace-insensitive. |
strings | Partial<Record<string, string>> | Overrides for every visible label, verdict sentence and announcement (plural forms as "one|other", placeholders like {names}, {delta}), including versus ("vs" between contradicting values), adjusted (the "adj." mark of corrected times) and sourceFallback ("Source {n}" for an unnamed source). English defaults. |
view / witness / tolerance / reference / selected / filter / locale / timeZone / precision / label / headingLevel / controls | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
select(key, { source?, focus? }) | Selects a fact (null clears). Emits the cancelable mv-select-fact; focus: true moves focus to its first dissenting entry (or the given source) and scrolls it into view. Returns false if unknown or vetoed. |
setOffset(sourceId, value) | Corrects a source's clock (ms or duration string). Emits the cancelable mv-offset-change, re-judges every fact and announces the new summary. |
suggestOffset(sourceId) | Estimated correction for a source: median gap between its times and the other sources' times on the facts they share, rounded to the second. null when it shares nothing. |
alignClocks() | Applies suggestOffset to every source except the reference (two passes) and returns the offsets. Real disagreements survive because the median ignores a minority of outliers. |
refresh() | Re-reads the markup (normally automatic through a MutationObserver). |
Events
| Name | Description |
|---|---|
mv-select-fact | Cancelable, before a fact is selected (click, Enter/Space, summary link, select()). detail: { key, fact, source (id of the entry clicked, or null), entry }. |
mv-view-change | Cancelable, before the view changes. detail: { view, previous, witness }. |
mv-offset-change | Cancelable, before a clock correction is applied. detail: { source, offset, previous, reason: "input" | "align" | "reset" | "api" }. |
mv-reconcile | After every analysis (data, tolerance or offset change). detail: the summary counts. |
Content structure
| Name | Description |
|---|---|
(sources) | Child <ol data-source> / <ul data-source> lists of <li> entries. Entry content (inline markup such as <code> or links) is cloned into the lanes; ids are stripped from the clones. |
CSS classes
| Name | Description |
|---|---|
mv-accounts-bar / -views / -view / -tool / -pick | Toolbar: view radio group, witness picker, “Only disagreements” and Clocks toggles. |
mv-accounts-clocks / -clock-row / -offset | Clock correction panel and its per-source offset inputs. |
mv-accounts-summary / -details / -jumps / -jump | Summary line with counts and the list of disagreements as jump links. |
mv-accounts-grid / -lane / -head / -list | Lanes layout (CSS grid with subgrid lanes so rows align across sources). The fact gutter is the lane with data-role="facts"; data-focused / data-dimmed in witness view. |
mv-accounts-entry / -fact / -link / -verdict | Entry cards (data-status: agree | dissent | split | single | silent, data-kind: time | value), gutter facts, links across lanes and the verdict row. data-hot / data-linked mark every piece of the hovered or focused fact. |
mv-accounts-merged / -card / -claims / -claim / -consensus / -dot | Merged view: fact cards on a rail, their claims per source and the consensus meter (circle = agrees, square = contradicts, dashed = silent). |
mv-accounts-glyph | Status shapes: check disc (agree), crossed diamond (conflict), half disc (single source), dashed ring (silent). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-accounts-agree | var(--mv-success) | Agreement tone. |
--mv-accounts-conflict | var(--mv-danger) | Conflict tone. |
--mv-accounts-single | var(--mv-warning) | Tone of facts only one source mentions. |
--mv-accounts-silent | var(--mv-fg-subtle) | Tone of silent cells. |
--mv-accounts-gutter | 12.5rem | Width of the fact gutter (9.5rem in narrow containers). |
--mv-accounts-lane-min | 12.5rem | Minimum lane width; below it the lanes scroll horizontally with the gutter sticky. |
--mv-accounts-max-height | none | Caps the height of the lanes / merged list; the lane headers then stay sticky while scrolling. |
Accessibility
Every lane is a <section> with a real heading (level set by heading-level) and an ordered list, the fact gutter included, so screen reader users can browse each account by heading and list. Each entry is a list item that names its fact and ends with a full verdict sentence (“Places it 4 min later than API gateway and Mobile app (2:02:31 PM).”, “Silent here; API gateway and Mobile app mention it.”, the reported time and correction when a clock was adjusted); the short visible labels (Agrees, +4 min, Contradicts, Only source, Silent) are paired with distinct shapes (check disc, crossed diamond, half disc, dashed ring) and never rely on color. The body is one tab stop with a roving tabindex: arrows move within a lane and across lanes on the same fact row, Alt+Left/Right jumps to the same fact in the next source that mentions it (skipping silent ones), Alt+Up/Down to the previous or next disagreement, Home/End and Ctrl+Home/End to the ends, Enter or Space selects a fact and Escape clears it; these instructions are exposed as the group's description. Hovering or focusing any piece of a fact highlights its entries and link in every lane. The view switcher is a radio group (arrow keys), the filter a toggle button (aria-pressed), Clocks a disclosure (aria-expanded) whose offset inputs are labelled per source, accept durations like -2m40s and step with Up/Down (1 s, Shift 1 min) and PageUp/PageDown. The summary is a native <details> whose items are buttons that move focus to the disagreement. View changes, selections (with their verdict) and clock corrections (with the new summary) are announced in a polite live region; focus is kept on the same fact across re-renders. The lanes of other sources in witness view lose their color but keep full text contrast. Reduced motion (OS or data-motion="reduce"): no FLIP reordering, no status flash, instant scrolling. Forced colors: shapes use CanvasText, selection, the current summary item and active controls use Highlight outlines, conflicts get a thicker border. Known limits: a review surface for tens of facts, not thousands (every re-analysis rebuilds the lanes); one entry per source and fact (a second one is ignored with a console warning); below the lane minimum width the lanes scroll sideways with the fact gutter sticky; data-motion="reduce" stops the FLIP reordering and smooth scrolling only when set on <html> (on another ancestor it still stops the CSS flash).