ExklusivBeta
Theseus <mv-theseus>
Herkunfts-Overlay, das die Frage beantwortet: „Wie viel davon ist noch das, was wir unterschrieben haben?“ Gib ihm eine Baseline und spätere Revisionen (vollständige Texte mit Autor, Datum und Label über die Eigenschaft revisions oder <template>-Kinder), und es verfolgt die Abstammung jedes Wortes: Jede Revision wird mit einem lokalen zweistufigen Myers-Diff (erst Absätze, dann Wörter) mit der vorherigen verglichen, sodass jedes Wort die Revision kennt, die es eingeführt hat, und jedes entfernte Wort die Revision, die es gestrichen hat. Der aktuelle Text wird in Schichten gemalt: Originalwörter bleiben schlicht, ersetzte oder hinzugefügte Passagen werden unterstrichen und nach Alter (neu = kräftig, alt = verblasst) oder nach Autor eingefärbt, entfernte Passagen werden zu schmalen schraffierten Lückenmarkierungen, die sich öffnen und den gestrichenen Text zeigen. Eine Anzeige im Header (role=meter) liest „38% original · 62% replaced across 14 revisions“, mit einem Rumpf aus Planken, deren Größe dem entspricht, was jede Revision noch besitzt; eine Zeitleiste mit Ticks springt zu jeder Revision, und das Overlay folgt, „Show changes since…“ schränkt es auf jüngste Änderungen ein, eine Hover-Karte nennt Art, Autor, Datum, Notiz und den ersetzten Wortlaut, und eine Schiene neben jedem Absatz zeigt dessen eigene Schichten. Funktioniert mit Markdown-ähnlichem Text, den es selbst rendert, oder annotiert vorhandenes Markup direkt (Inline-Elemente bleiben erhalten) über Blöcke [data-theseus-block].
| Kategorie | Datenanzeige |
|---|---|
| Typ | Web Component (<mv-theseus>) |
| Status | Beta |
| Installiert auch | 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
Installation
node scripts/add.mjs theseus --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["theseus"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): 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.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<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>Independently tested for harmful substances</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>Kulturelle Referenz
Leben des Theseus (Parallelbiographien), Plutarch (75, Buch). Die Athener bewahrten das Schiff des Theseus, indem sie seine morschen Planken eine nach der anderen ersetzten, bis Philosophen fragten, ob ein Schiff, dessen Planken alle ersetzt sind, noch dasselbe Schiff ist. In der UI ist jedes Wort eines Dokuments eine Planke mit Abstammung: Das Overlay zeigt, welche Planken original sind, welche ersetzt wurden, wann und von wem, und eine Anzeige misst, wie viel vom ursprünglichen Schiff noch übrig ist.
API
Attributes
| Name | Typ | 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 | Typ | 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.