Exclusifbêta

Theseus <mv-theseus>

Surcouche de provenance qui répond à la question « que reste-t-il de ce que nous avons signé ? ». Donnez-lui une version de référence et des révisions ultérieures (textes complets avec auteur, date et libellé via la propriété revisions ou des enfants <template>) et elle suit la lignée de chaque mot : chaque révision est comparée à la précédente avec un diff de Myers local à deux niveaux (paragraphes d’abord, puis mots), si bien que chaque mot connaît la révision qui l’a introduit et chaque mot supprimé la révision qui l’a retiré. Le texte actuel est peint en strates : les mots d’origine restent sans style, les passages remplacés ou ajoutés sont soulignés et teintés selon leur âge (récent = marqué, ancien = estompé) ou selon leur auteur, et les passages supprimés deviennent de fins repères hachurés qui s’ouvrent pour montrer le texte barré. Une jauge d’en-tête (role=meter) indique « 38 % d’origine · 62 % remplacé au fil de 14 révisions », avec une coque de planches dimensionnées selon ce que chaque révision possède encore ; une frise graduée permet d’aller à n’importe quelle révision et la surcouche suit, « Afficher les changements depuis… » la restreint aux modifications récentes, une carte au survol donne le type, l’auteur, la date, la note et la formulation remplacée, et un rail à côté de chaque paragraphe montre ses propres strates. Fonctionne sur du texte de type markdown qu’elle affiche elle-même, ou annote sur place un balisage existant (éléments inline conservés) via des blocs [data-theseus-block].

CatégorieAffichage de données
TypeWeb Component (<mv-theseus>)
Statutbêta
Installe aussibutton, 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

  • 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

Installation

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

Agent IA avec le serveur MCP Marvelous UI : install_components({ slugs: ["theseus"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : 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.

Utilisation

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :

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

Référence culturelle

Vie de Thésée (Vies parallèles), Plutarque (75, livre). Les Athéniens conservèrent le navire de Thésée en remplaçant ses planches pourries une à une, jusqu’à ce que des philosophes se demandent si un navire dont toutes les planches ont été remplacées est encore le même navire. Dans l’interface, chaque mot d’un document est une planche avec sa lignée : la surcouche montre quelles planches sont d’origine, lesquelles ont été remplacées, quand et par qui, et une jauge mesure ce qui reste du navire d’origine.

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.

Cette page a été traduite par IA. Signaler un problème de traduction