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.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-theseus>) |
| Status | beta |
| Also installs | button, select |
| Keywords | exclusive, culture, diff, provenance, revision-history, version-history, blame, track-changes, contract, policy, terms, audit, compliance, timeline, authorship, document, changes-since, meter |
When to use
- A signed contract, policy or terms page keeps being edited and reviewers must see what still matches the signed version
- A long spec, docs page or prompt edited by many people needs to show who changed which passage, and when
- Legal or compliance has to review only what changed since the last approval, not reread the whole document
- A product description or template maintained by several teams should reveal how much of the original copy survives
Avoid when
- Only the overall freshness of a page matters, not which parts changed → use Dorian instead
- Two structured records must be compared field by field (invoice vs order, form vs ID) → use Papers instead
- The content is source code or data where a line-based side-by-side diff with syntax is expected
Install
node scripts/add.mjs theseus --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
revision | number | revision id | Revision 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). | |
since | number | 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-by | age | author | age | age: 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. |
view | strata | text | strata | text: a plain, printable ins/del rendering: every removal open and struck through, changes underlined without tint, rails hidden. |
controls | space-separated list | gauge since timeline legend nav | Header 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-level | number | 3 | Text mode: level of the <h*> produced by “# ” (“## ” is one level deeper), to fit the host page outline. |
locale | BCP 47 tag | en-US | Date formatting. |
label | string | Original content | Accessible name of the gauge meter. |
data-theseus-block | string (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-revision | on <template> children | Declarative 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
| Name | Type | Description |
|---|---|---|
revisions | Array<{ 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. |
parts | Array<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 / label | reflected | Mirror the attributes. |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-revision-change | Cancelable, 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-part | Cancelable, 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
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-theseus-head | Header: .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-doc | Text mode: the rendered document (p, h*, ul/ol, strong, code). |
mv-theseus-ins | A changed run (<ins>, --_s = age strength 0.2…1, data-rev, data-author, data-selected, data-fresh while its entrance plays). |
mv-theseus-settled | A change at or before the since revision (dotted underline). |
mv-theseus-gap | A removal: -gap-toggle (button, aria-expanded, hatched sliver showing “−N” on hover) and -gap-text (<del>). data-kind="replaced | removed", data-open. |
mv-theseus-card | Hover / 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
| Name | Default | Description |
|---|---|---|
--mv-theseus-fresh | var(--mv-accent) | Hue of changes in age mode (strength follows age). |
--mv-theseus-original | fg mixed into bg | Original planks and rail segments. |
--mv-theseus-settled | fg 38% into bg | Changes older than the since revision. |
--mv-theseus-removed | var(--mv-danger) | Hatching of removal gaps and struck text. |
--mv-theseus-author-1 … -6 | accent, info, success, warning, magenta, teal | Author palette (color-by="author"), cycled in order of first appearance. |
--mv-theseus-rail-gap | 1rem | Distance 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.