独家

Accounts <mv-accounts>

多来源时间线,把同一事件的多份陈述对齐排列,逐条事实显示它们在哪里一致、在哪里互相矛盾、在哪里某个来源保持沉默:事故复盘(网关日志 vs 支付服务商 vs 客户端遥测 vs 值班记录)、配送纠纷(客户 vs 仓库 vs 承运商)、审计轨迹、事实核查、多位参会者的会议记录。

文化出处

罗生门,黑泽明(1950 年,电影)。同一事件由四位证人讲述了四遍,每个版本都合情合理,却又彼此矛盾,真相只能通过交叉比对来推断。在 UI 中,每个来源对某一事件的陈述各成一条泳道,相同的事实在各泳道间对齐,并被判定为一致、矛盾或无依据;故事可以并排阅读、合并阅读,或只按某一位证人的讲述阅读。

工作原理

多来源时间线,把同一事件的多份陈述对齐排列,逐条事实显示它们在哪里一致、在哪里相互矛盾、在哪里某个来源保持沉默:事故复盘(网关日志 vs 支付服务商 vs 客户端遥测 vs 值班记录)、配送纠纷(客户 vs 仓库 vs 承运商)、审计轨迹、事实核查、多位与会者的会议记录。每个来源都是一个普通的 <ol data-source>(或 sources 属性),其中带时间戳的 <li data-at data-key> 条目陈述事实;共享同一 key 的条目被对齐到同一行并给出判定:agree(一致)、conflict(不同的 data-value,或时间超出容差,按每条事实计算多数,平局由可选的参考来源裁定)或 only-one-source(仅一个来源),某一行缺少的来源显示为沉默。三种视图:Side by side(每个来源一条泳道,各行按事实对齐,泳道之间画出连线,并有一栏共识时间)、Merged(单一时间顺序,每条事实列出谁支持、谁反驳,以及一个共识度量表)和 One witness(按单一来源的讲述重新排序故事,其他来源去饱和显示在旁边)。各来源的时钟并不一致,因此每个来源都有一个偏移量,可在 Clocks 面板中编辑,或根据它与其他来源共享的事实估算(Match、Align all clocks);行通过 FLIP 重新排序,判定发生变化时会闪烁。摘要(“3 conflicts · 2 facts only one source mentions”)把每处分歧列为跳转链接,选中某条事实会在其所在行下方展开书面判定,每种状态都以文字写明并用不同的形状绘制,绝不只靠颜色区分。

分类数据展示
类型Web Component(<mv-accounts>)
状态稳定版
Keywordsexclusive, 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/marvelous

通过 Marvelous 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类型DefaultDescription
viewlanes | merged | witnesslaneslanes: 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.
witnesssource idSource focused by the witness view (default: the first source).
toleranceduration1mTimes 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).
referencesource idSystem 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).
selectedfact keySelected fact: highlighted in every view, with a written verdict under its row in the lanes views. Reflects user selection.
filterall | issuesallissues hides facts on which every source agrees (the “Only disagreements” toggle).
localeBCP 47en-USLocale for times, durations, plurals and name lists.
time-zoneIANA zoneZone used to display times (default: the viewer's; UTC when every time is wall-clock only). Shown in the gutter header.
precisionauto | seconds | minutesautoTime display precision; auto shows seconds when any entry has them.
labelstringAccountsAccessible name of the timeline group.
heading-level1-63Level of the lane headings.
controls"true" | "false"true"false" hides the built-in toolbar (views, filter, Clocks) when the app drives them.
data-sourcestringOn 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-descriptionstring / duration / stringOn 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-atISO 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-keystringOn an entry: the fact it claims. Same key across sources = same fact. Entries without a key are facts only their source mentions.
data-valuestringOn 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-tolerancestring / 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
sourcesArray<{ 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.
factsArray<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).
offsetsRecord<sourceId, ms>Current clock corrections (read-only; use setOffset).
factLabelsRecord<key, { label?, tolerance? }>Titles and tolerances per fact, when the markup cannot carry them.
compare(a: string, b: string, key: string) => booleanCustom value equality (numeric tolerance, synonyms…). Default: case-, accent- and whitespace-insensitive.
stringsPartial<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 / controlsreflectedMirror the attributes.

Methods

NameDescription
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

NameDescription
mv-select-factCancelable, 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-changeCancelable, before the view changes. detail: { view, previous, witness }.
mv-offset-changeCancelable, before a clock correction is applied. detail: { source, offset, previous, reason: "input" | "align" | "reset" | "api" }.
mv-reconcileAfter every analysis (data, tolerance or offset change). detail: the summary counts.

Content structure

NameDescription
(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

NameDescription
mv-accounts-bar / -views / -view / -tool / -pickToolbar: view radio group, witness picker, “Only disagreements” and Clocks toggles.
mv-accounts-clocks / -clock-row / -offsetClock correction panel and its per-source offset inputs.
mv-accounts-summary / -details / -jumps / -jumpSummary line with counts and the list of disagreements as jump links.
mv-accounts-grid / -lane / -head / -listLanes 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 / -verdictEntry 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 / -dotMerged view: fact cards on a rail, their claims per source and the consensus meter (circle = agrees, square = contradicts, dashed = silent).
mv-accounts-glyphStatus shapes: check disc (agree), crossed diamond (conflict), half disc (single source), dashed ring (silent).

CSS variables

NameDefaultDescription
--mv-accounts-agreevar(--mv-success)Agreement tone.
--mv-accounts-conflictvar(--mv-danger)Conflict tone.
--mv-accounts-singlevar(--mv-warning)Tone of facts only one source mentions.
--mv-accounts-silentvar(--mv-fg-subtle)Tone of silent cells.
--mv-accounts-gutter12.5remWidth of the fact gutter (9.5rem in narrow containers).
--mv-accounts-lane-min12.5remMinimum lane width; below it the lanes scroll horizontally with the gutter sticky.
--mv-accounts-max-heightnoneCaps 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).

本页面由 AI 翻译。报告翻译问题