Exclusivebeta

Theseus — <mv-theseus>

Provenance overlay that answers “how much of this is still what we signed?”. Give it a baseline and later revisions (full texts with author, date and label via the revisions property or <template> children) and it follows the lineage of every word: each revision is diffed against the previous one with a local two-level Myers diff (paragraphs first, then words), so every word knows the revision that introduced it and every removed word the revision that took it out. The current text is painted as strata: original words stay plain, replaced or added runs are underlined and tinted by age (recent = strong, old = faded) or by author, removed runs become thin hatched gap markers that open to show the struck text. A header gauge (role=meter) reads “38% original · 62% replaced across 14 revisions”, with a hull of planks sized by what each revision still owns; a tick timeline scrubs to any revision and the overlay follows, “Show changes since…” narrows it to recent edits, a hover card gives kind, author, date, note and the replaced wording, and a rail beside every paragraph shows its own strata. Works on markdown-ish text it renders itself, or annotates existing markup in place (inline elements kept) through [data-theseus-block] blocks.

CategoryData display
TypeWeb Component (<mv-theseus>)
Statusbeta
Also installsbutton, select
Keywordsexclusive, culture, diff, provenance, revision-history, version-history, blame, track-changes, contract, policy, terms, audit, compliance, timeline, authorship, document, changes-since, meter

When to use

Avoid when

Install

node scripts/add.mjs theseus --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["theseus"], 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/pointer.js, core/position.js, components/theseus/theseus.js, components/theseus/theseus.css, components/button/button.css, components/select/select.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="th-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #th-demo { display:grid; gap:1.5rem }
    #th-demo .th-app { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #th-demo .th-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.875rem 1.25rem; border-bottom:1px solid var(--mv-border); background:var(--mv-bg-subtle) }
    #th-demo .th-title { display:grid; gap:.125rem; min-width:0 }
    #th-demo .th-title small { color:var(--mv-fg-muted); font-size:.75rem }
    #th-demo .th-title strong { font-size:.9375rem; letter-spacing:-.01em }
    #th-demo .th-switches { display:flex; align-items:center; gap:.5rem 1.25rem; flex-wrap:wrap }
    #th-demo .th-switches .mv-choice { font-size:.8125rem }
    #th-demo .th-body { padding:1.25rem 1.5rem 1.5rem }
    #th-demo .th-log { display:flex; align-items:center; gap:.5rem; min-height:2.5rem; margin:0; padding:.625rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle); color:var(--mv-fg-muted); font-size:.75rem }
    #th-demo .th-log b { color:var(--mv-fg); font-weight:500 }
    #th-demo .th-pim { display:grid; grid-template-columns:minmax(0,1fr) 15rem; gap:1.25rem; align-items:start; padding:1.25rem 1.5rem 1.5rem }
    #th-demo .th-product h4 { margin:0 0 .375rem; font-size:1.0625rem; letter-spacing:-.01em }
    #th-demo .th-product p { margin:0 0 .75rem; font-size:.875rem }
    #th-demo .th-product ul { margin:0; padding-inline-start:1.125rem; font-size:.875rem }
    #th-demo .th-product mv-theseus { --mv-theseus-rail-gap:.75rem }
    #th-demo .th-product .mv-theseus-head { padding:.875rem 1rem; margin-block-end:1rem }
    #th-demo .th-product .mv-theseus-pct { font-size:var(--mv-text-2xl) }
    #th-demo .th-side { display:grid; gap:.625rem; padding:1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); font-size:.8125rem }
    #th-demo .th-side dl { display:grid; grid-template-columns:auto 1fr; gap:.375rem .75rem; margin:0 }
    #th-demo .th-side dt { color:var(--mv-fg-muted) }
    #th-demo .th-side dd { margin:0; text-align:end; font-variant-numeric:tabular-nums }
    #th-demo .th-side p { margin:0; color:var(--mv-fg-muted); font-size:.75rem; line-height:1.5 }
    @media (max-width:44rem) {
      #th-demo .th-pim { grid-template-columns:minmax(0,1fr) }
      #th-demo .th-body, #th-demo .th-pim { padding:1rem }
    }
  </style>

  <!-- 1. A signed policy edited 14 times since signature: how much of what was signed is left? -->
  <section class="th-app" aria-labelledby="th-policy-title">
    <header class="th-bar">
      <span class="th-title">
        <small>Northwind Cloud · Customer Terms · Section 7</small>
        <strong id="th-policy-title">Refunds and cancellations</strong>
      </span>
      <span class="th-switches">
        <span class="mv-badge" data-variant="secondary">Signed Mar 4, 2026</span>
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="th-author">
          <span class="mv-choice-text"><span class="mv-choice-title">Color by author</span></span>
        </label>
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="th-text">
          <span class="mv-choice-text"><span class="mv-choice-title">Text view</span></span>
        </label>
      </span>
    </header>
    <div class="th-body">
      <mv-theseus id="th-policy" heading-level="4" label="Share of the signed version still in place"></mv-theseus>
    </div>
    <p class="th-log" id="th-log" aria-live="polite">Hover a highlighted passage to see who changed it and when · <kbd class="mv-kbd">n</kbd> / <kbd class="mv-kbd">p</kbd> jump between changes · <kbd class="mv-kbd">[</kbd> / <kbd class="mv-kbd">]</kbd> step revisions</p>
  </section>

  <!-- 2. Existing markup annotated in place: a product description edited by several teams. -->
  <section class="th-app" aria-labelledby="th-pim-title">
    <header class="th-bar">
      <span class="th-title">
        <small>Catalog · SKU LN-2210-SG</small>
        <strong id="th-pim-title">Product description</strong>
      </span>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="th-expand">Show all removed text</button>
    </header>
    <div class="th-pim">
      <div class="th-product">
        <mv-theseus id="th-pim" color-by="author" controls="gauge legend nav">
          <template data-theseus-revision data-author="Lena Fischer" data-date="2026-01-12" data-label="Launch copy">
            <h4 data-theseus-block="title">Linen Duvet Cover, Sage</h4>
            <p data-theseus-block="intro">Made from <strong>100% European flax linen</strong>, this duvet cover gets softer with every wash. Its relaxed, lived-in texture looks great unmade.</p>
            <ul data-theseus-block="features"><li>Stonewashed for a soft hand feel</li><li>Hidden button closure</li><li>Machine washable at 40°C</li></ul>
          </template>
          <template data-theseus-revision data-author="Diego Morales" data-date="2026-04-03" data-label="SEO pass">
            <h4 data-theseus-block="title">Stonewashed Linen Duvet Cover, Sage</h4>
            <p data-theseus-block="intro">Made from <strong>100% European flax linen</strong>, this breathable duvet cover gets softer with every wash. Its relaxed, lived-in texture looks great unmade.</p>
          </template>
          <template data-theseus-revision data-author="Hannah Okafor" data-date="2026-06-19" data-label="Care instructions from QA">
            <ul data-theseus-block="features"><li>Stonewashed for a soft hand feel</li><li>Hidden coconut-shell button closure</li><li>Machine washable at 40°C (104°F), tumble dry low</li></ul>
          </template>
          <template data-theseus-revision data-author="Sofia Rossi" data-date="2026-09-08" data-label="Fall campaign"></template>

          <h4 data-theseus-block="title">Stonewashed Linen Duvet Cover, Sage</h4>
          <p data-theseus-block="intro">Woven from <strong>100% European flax linen</strong>, this breathable duvet cover keeps you cool in summer and cozy in winter. Its relaxed, lived-in texture looks great unmade.</p>
          <ul data-theseus-block="features"><li>Stonewashed for a soft hand feel</li><li>Hidden coconut-shell button closure</li><li>Machine washable at 40°C (104°F), tumble dry low</li><li>OEKO-TEX® certified</li></ul>
        </mv-theseus>
      </div>
      <aside class="th-side" aria-label="Listing details">
        <dl>
          <dt>Price</dt><dd>$189.00</dd>
          <dt>Sizes</dt><dd>Full/Queen, King</dd>
          <dt>Last edit</dt><dd>Sep 8, 2026</dd>
        </dl>
        <p>This card is ordinary product markup (heading, bold text, list) annotated in place: its structure is kept, only the changed words are wrapped.</p>
      </aside>
    </div>
  </section>

  <script type="module">
    const policy = document.getElementById("th-policy");
    const log = document.getElementById("th-log");

    const baseline = `You may cancel your subscription at any time from the Billing page. Cancellation takes effect at the end of the current billing period, and you keep access to paid features until then.

# Refunds

Annual plans purchased directly from us are eligible for a full refund within 14 days of the initial purchase. Monthly plans are not refundable. Refunds are issued to the original payment method within 10 business days.

# Exceptions

- Add-ons and one-time services are not refundable once delivered.
- Accounts suspended for violating the Acceptable Use Policy are not eligible for a refund.
- Purchases made through a reseller are refunded by that reseller.

# Downgrades

If you downgrade to a lower plan, the difference is credited to your account and applied to future invoices. Credits expire after 12 months and cannot be exchanged for cash.

# Contact

Questions about billing can be sent to our billing team. We answer within two business days.`;

    // Each revision is a set of edits applied to the previous text.
    const edits = [
      ["2026-04-02", "Mateo Alvarez", "Longer refund window", [["within 14 days of the initial purchase", "within 30 days of the initial purchase"]]],
      ["2026-04-18", "Priya Raman", "Clarify how to cancel", [["from the Billing page", "from Settings → Billing, or by contacting support"]]],
      ["2026-05-06", "Jonas Weber", "Add renewals clause", [["within 10 business days.", "within 10 business days.\n\nRenewals of annual plans can be refunded within 7 days of the renewal date if the service was not used during that period."]]],
      ["2026-05-21", "Aiko Tanaka", "Monthly plans", [["Monthly plans are not refundable.", "Monthly plans can be canceled at any time, but the current month is not prorated."]]],
      ["2026-06-09", "Mateo Alvarez", "Payment method wording", [["Refunds are issued to the original payment method within 10 business days.", "Approved refunds go back to the original payment method; banks usually post them within 5 to 10 business days."]]],
      ["2026-06-24", "Chloé Martin", "EU right of withdrawal", [["refunded by that reseller.", "refunded by that reseller.\n- Customers in the European Union keep their statutory 14-day right of withdrawal."]]],
      ["2026-07-08", "Priya Raman", "Credits never expire", [["Credits expire after 12 months and cannot be exchanged for cash.", "Credits never expire, but they cannot be exchanged for cash or transferred to another account."]]],
      ["2026-07-22", "Jonas Weber", "Partner purchases", [["Purchases made through a reseller are refunded by that reseller.", "Purchases made through an authorized partner follow that partner’s refund terms."]]],
      ["2026-08-12", "Aiko Tanaka", "Support channel", [["Questions about billing can be sent to our billing team. We answer within two business days.", "Billing questions can be sent from the Help Center; our team replies within one business day."]]],
      ["2026-08-27", "Kwame Mensah", "Termination terms", [["Accounts suspended for violating the Acceptable Use Policy", "Accounts terminated for a material breach of these Terms"]]],
      ["2026-09-03", "Aiko Tanaka", "Clarify refund window", [["are eligible for a full refund within 30 days of the initial purchase", "can be refunded in full within 30 days of the first payment, no questions asked"]]],
      ["2026-09-10", "Chloé Martin", "Access after cancellation", [["and you keep access to paid features until then.", "and your workspace stays fully available until then; after that it moves to the Free plan."]]],
      ["2026-09-15", "Mateo Alvarez", "One-time services", [["Add-ons and one-time services are not refundable once delivered.", "One-time services such as onboarding workshops are not refundable once delivered."]]],
      ["2026-09-18", "Kwame Mensah", "Legal review", [["# Downgrades", "# Plan changes"], ["If you downgrade to a lower plan,", "When you move to a lower plan,"]]],
    ];

    const revisions = [{ id: "signed", text: baseline, author: "Legal team", date: "2026-03-04", label: "Signed version" }];
    let text = baseline;
    edits.forEach(([date, author, label, ops], i) => {
      for (const [from, to] of ops) text = text.replace(from, to);
      revisions.push({ id: `r${i + 1}`, text, author, date, label });
    });
    policy.revisions = revisions;

    document.getElementById("th-author").addEventListener("change", (e) => { policy.colorBy = e.target.checked ? "author" : "age"; });
    document.getElementById("th-text").addEventListener("change", (e) => { policy.view = e.target.checked ? "text" : "strata"; });

    const say = (...parts) => {
      log.replaceChildren(...parts.map((p) => (typeof p === "string" ? p : Object.assign(document.createElement("b"), { textContent: p.b }))));
    };
    policy.addEventListener("mv-select-part", (e) => {
      const { description, removed, text: now } = e.detail.part;
      say({ b: description }, removed && now ? ` · was “${removed.slice(0, 60)}${removed.length > 60 ? "…" : ""}”` : removed ? ` · “${removed.slice(0, 60)}…”` : "");
    });
    policy.addEventListener("mv-revision-change", (e) => {
      const { index, revision } = e.detail;
      say(index === 0 ? "Showing the " : "Showing ", { b: index === 0 ? "signed version" : `revision ${index}` }, ` · ${revision.label} · ${revision.author}`);
    });

    const pim = document.getElementById("th-pim");
    const expand = document.getElementById("th-expand");
    let open = false;
    expand.addEventListener("click", () => {
      open = !open;
      pim.expandAll(open);
      expand.textContent = open ? "Hide removed text" : "Show all removed text";
    });
  </script>
</div>

Cultural reference

Life of Theseus (Parallel Lives) — Plutarch (75, book). The Athenians preserved Theseus’s ship by replacing its rotting planks one at a time, until philosophers asked whether a ship with every plank replaced is still the same ship. In the UI, every word of a document is a plank with a lineage: the overlay shows which planks are original, which were replaced, when and by whom, and a gauge measures how much of the original vessel remains.

API

Attributes

NameTypeDefaultDescription
revisionnumber | revision idRevision shown (0 = baseline; negative counts from the end). Absent = latest, and stays on the latest when revisions are added. Reflects user scrubbing (removed again when the latest is reached).
sincenumber | revision id“Show changes since”: words introduced at or before this revision are treated as settled (dotted underline, no tint, not navigable), gaps are computed against it, the rest of the text is muted and the gauge adds “N% changed since …”. Absent = the baseline.
color-byage | authorageage: one hue whose strength follows the age of each change (time-based between the since revision and the shown one, revision rank when dates are missing). author: one color per author (6-color palette, cycled), legend lists the authors.
viewstrata | textstratatext: a plain, printable ins/del rendering: every removal open and struck through, changes underlined without tint, rails hidden.
controlsspace-separated listgauge since timeline legend navHeader parts to show. The timeline needs 2 revisions, the since filter 3. An empty value hides the header.
rail"false""false" removes the strata rail drawn beside every paragraph (text mode) or block (markup mode).
heading-levelnumber3Text mode: level of the <h*> produced by “# ” (“## ” is one level deeper), to fit the host page outline.
localeBCP 47 tagen-USDate formatting.
labelstringOriginal contentAccessible name of the gauge meter.
data-theseus-blockstring (key)On existing elements inside the component: switches to markup mode. Each block keeps its own lineage; the live markup is always the latest revision and is painted in place (text nodes split, inline elements kept, restored on removal). In revision templates, the element with the same key holds that block’s content at that revision (missing = unchanged).
data-theseus-revisionon <template> childrenDeclarative revisions in order, first = baseline. data-author, data-date (ISO day or datetime), data-label, data-id. Text mode: the template text is the whole document (markdown-ish). Markup mode: the template holds [data-theseus-block] elements; the last template can be empty (it only dates the live markup).

Properties

NameTypeDescription
revisionsArray<{ id?, text?, blocks?: Record<key, string | Node>, author?, date?: string | Date | number, label? }>Revisions in order, first = baseline (two entries = baseline vs current). text for text mode (paragraphs, # headings, - / 1. lists, bold, code), blocks for markup mode. Setting it replaces the templates and re-renders.
stats{ words, original, baselineWords, originalShare, changedSince, revisions, byRevision: number[], byAuthor: Record<string, number> }Word counts for the shown revision (read-only). originalShare = original / words.
partsArray<Part>Navigable changes of the shown state, in reading order (read-only). Part = { kind: "added" | "replaced" | "removed", revision, id, author, date, label, text, removed, block, description }.
revision / since / colorBy / view / controls / rail / headingLevel / locale / labelreflectedMirror the attributes.

Methods

NameDescription
goTo(indexOrId)Shows a revision without emitting mv-revision-change. Returns false if it does not exist.
nextChange() / previousChange()Moves focus to the next / previous change (wraps), scrolls it into view and shows its card. Returns false when there is none.
expandAll(open = true)Opens (or closes) every removed-text gap of the current view; stays applied across re-renders until called again.
refresh()Re-reads the templates and the live markup (call it after the app edited the annotated blocks).

Events

NameDescription
mv-revision-changeCancelable, before the user moves to another revision (timeline drag or click, arrow buttons, keyboard). detail: { index, previous, revision: { id, author, date, label }, source: "pointer" | "keyboard" | "button" }. preventDefault() keeps the current revision (e.g. while unsaved edits exist).
mv-select-partCancelable, when a change is clicked or activated with Enter / Space. detail: { part (see parts), element }. preventDefault() stops the card from being pinned (open your own review panel instead).

Content structure

NameDescription
template[data-theseus-revision]Revision templates (inert, never rendered).
(content)Markup mode: any markup containing [data-theseus-block] elements; it stays yours and is only annotated. Text mode: the component renders the document in .mv-theseus-doc.

CSS classes

NameDescription
mv-theseus-headHeader: .mv-theseus-gauge (role=meter: -pct, -summary, -kept, -delta, .mv-theseus-hull of .mv-theseus-plank), .mv-theseus-since (native select), .mv-theseus-timeline (-track role=slider with -tick and -thumb, -caption), .mv-theseus-foot (-legend, -nav with -counter).
mv-theseus-docText mode: the rendered document (p, h*, ul/ol, strong, code).
mv-theseus-insA changed run (<ins>, --_s = age strength 0.2…1, data-rev, data-author, data-selected, data-fresh while its entrance plays).
mv-theseus-settledA change at or before the since revision (dotted underline).
mv-theseus-gapA removal: -gap-toggle (button, aria-expanded, hatched sliver showing “−N” on hover) and -gap-text (<del>). data-kind="replaced | removed", data-open.
mv-theseus-cardHover / focus card (top-layer popover, aria-hidden): -card-kind, -card-rev, -card-who, -card-when, -card-note, -card-was. data-pinned after a click.
[data-theseus-rail]Paragraph or block carrying a strata rail (::before, gradient in --_rail).

CSS variables

NameDefaultDescription
--mv-theseus-freshvar(--mv-accent)Hue of changes in age mode (strength follows age).
--mv-theseus-originalfg mixed into bgOriginal planks and rail segments.
--mv-theseus-settledfg 38% into bgChanges older than the since revision.
--mv-theseus-removedvar(--mv-danger)Hatching of removal gaps and struck text.
--mv-theseus-author-1 … -6accent, info, success, warning, magenta, tealAuthor palette (color-by="author"), cycled in order of first appearance.
--mv-theseus-rail-gap1remDistance between the text and its strata rail (also the inline-start padding reserved for it).

Accessibility

Provenance never relies on color alone: changed runs are real <ins> elements with a solid underline whose thickness follows age, settled changes use a dotted underline, removals are hatched <button> gaps that open a <del>. Each change carries visually hidden text read in the flow (“Replaced on Sep 3 by Aiko Tanaka: … (end of change)”), and each gap button has a full name (“Show removed text, 12 words. Removed on Aug 27 by Kwame Mensah”) plus aria-expanded; the struck text is announced with a “Removed text:” prefix. The gauge is role="meter" (0–100, aria-valuetext “38% original, 62% replaced across 14 revisions.”); planks, ticks and rails are aria-hidden. The timeline is a role="slider" (Arrow keys step one revision, Page Up/Down a fifth, Home/End baseline/latest, aria-valuetext with revision, date, author and note) flanked by real Previous/Next buttons; a polite status announces the revision reached. Changes form a single tab stop with a roving tabindex: Arrow keys move between changes, Home/End first/last, Enter or Space pins the card (and opens the replaced text), Escape unpins; n / p (or j / k) jump between changes and [ / ] step revisions from anywhere inside the component (ignored in form fields); Previous/Next change buttons and a “Change 3 of 12” counter do the same for pointer users. The hover card is aria-hidden because everything in it is already in the text. “Show changes since” is a labelled native select. view="text" gives a plain ins/del rendering for print or for readers who prefer no tints. Forced colors: underlines use Highlight, gaps a dashed ButtonText border, rails are hidden. Reduced motion (OS or data-motion="reduce"): no entrance flash for fresh changes, no card slide, scrolling to a change is instant.