ExklusivBeta

Unsourced <mv-unsourced>

Belegprüfung auf Ebene einzelner Aussagen für generierte oder lange Texte (KI-Antworten, Support-Antworten, Recherche-Zusammenfassungen, Berichte): Jede Aussage zeigt für sich, direkt an Ort und Stelle, ob etwas sie stützt. Umschließe Text, dessen Aussagen mit <mark data-claim> markiert sind (oder gib ihm Text plus Aussage-Spans per Offset oder exakter Übereinstimmung, in der Form, die Grounding-APIs liefern) und eine Quellenbibliothek; jede Aussage ist sourced, weak, contradicted, unsourced, opinion oder checking, abgeleitet aus ihren Belegen (source, quote, stance supports | partial | contradicts, score) oder explizit gesetzt. Belegte Aussagen bekommen kleine nummerierte Quellen-Chips, nummeriert wie Fußnoten; eine unbelegte Aussage lässt an ihrem letzten Wort einen leisen Haarlinien-Schweif wachsen, umso länger, je mehr von ihr auf nichts steht (Länge der Aussage × fehlende Stützung), mit einer eigenen Form pro Status (offener Ring = keine Quelle, Stoppbalken = widerlegt, gepunktet = schwach) und einem „No source“-Tag bei Hover oder Fokus, und der Schweif zieht sich in einen Chip zurück, sobald eine Quelle angehängt wird. Hover, Fokus oder ein Chip öffnet die Belege: jede Quelle mit ihrem passenden Zitat, die mit der Aussage geteilten Wörter hervorgehoben; nicht gestützte Aussagen bieten Find a source (abbrechbares mv-find-source, dessen waitUntil(promise) die App eine Suche ausführen und das Gefundene anhängen lässt), Mark as opinion und Remove claim (direkt rückgängig zu machen, und toText() liefert den Text ohne sie). Eine Übersichtsleiste liest „8 claims · 3 sourced · 1 weak · 2 unsourced“ über einer Abdeckungsanzeige, springt durch die Aussagen, die eine Quelle brauchen (Tasten [ und ]), und kann alles andere abdunkeln; eine optionale nummerierte Sources-Liste schließt den Text ab.

KategorieDatenanzeige
TypWeb Component (<mv-unsourced>)
StatusBeta
KitKI, die du überprüfen kannst
Installiert auchbutton
Keywordsexclusive, culture, ai, llm, grounding, citations, sources, claims, fact-check, hallucination, verification, evidence, provenance, footnotes, review, support-reply, rag, annotation

When to use

  • An AI-drafted support reply or email must be checked for promises and facts that no policy or record backs before it is sent
  • An AI answer or research summary should show which statements are cited, weakly supported, contradicted or unsupported
  • A grounding or fact-check service returns claim spans with evidence and the text needs inline citations plus a review pass
  • Reviewers must step through unsupported claims one by one and fix, soften or remove each without leaving the text

Avoid when

  • The model returns a confidence score per passage rather than sources to check → use Certainty instead
  • Several accounts of the same events must be reconciled fact by fact on a timeline → use Accounts instead
  • The need is to show how a single figure, chart or score was computed → use Curtain instead

Installation

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

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["unsourced"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/unsourced/unsourced.js, components/unsourced/unsourced.css, components/button/button.css.

Verwendung

Schnellstart mit dem kleinsten funktionierenden Markup:

<mv-unsourced>
  <script type="application/json" data-sources>[{ "id": "returns", "title": "Returns policy" }]</script>
  <p><mark data-claim data-cites="returns">Damaged items are refunded within 30 days</mark>, and <mark data-claim>we'll waive the $12 restocking fee</mark>.</p>
</mv-unsourced>

Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:

<div id="us-demo" style="width:min(100%,50rem);margin-inline:auto">
  <style>
    #us-demo { display:grid; gap:2rem }
    #us-demo .us-card { display:grid; gap:1rem; padding:1.25rem 1.5rem 1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #us-demo .us-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap }
    #us-demo .us-head h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
    #us-demo .us-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #us-demo .us-ask { margin:0; padding:.7rem .9rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-muted); color:var(--mv-fg); font-size:.875rem; line-height:1.5 }
    #us-demo .us-ask b { display:block; margin-bottom:.15rem; color:var(--mv-fg-muted); font-size:.75rem; font-weight:600 }
    #us-demo mv-unsourced { font-size:.975rem }
    #us-demo mv-unsourced p { margin:0 0 .75rem }
    #us-demo .us-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding-top:1rem; border-top:1px solid var(--mv-border) }
    #us-demo .us-state { display:flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #us-demo .us-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #us-demo .us-foot .mv-choice { font-size:.8125rem }
    #us-demo details { font-size:.8125rem; color:var(--mv-fg-muted) }
    #us-demo summary { cursor:pointer; width:max-content; border-radius:var(--mv-radius-sm) }
    #us-demo summary:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #us-demo pre { margin:.5rem 0 0; padding:.75rem .9rem; border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); color:var(--mv-fg); font:.8125rem/1.55 var(--mv-font-sans); white-space:pre-wrap }
    #us-demo .us-hint { margin:-.75rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
    #us-demo kbd { padding:0 .3em; border:1px solid var(--mv-border); border-bottom-width:2px; border-radius:4px; font:600 .6875rem/1.4 var(--mv-font-mono); color:var(--mv-fg) }
  </style>

  <!-- 1 · AI-drafted support reply, annotated markup -->
  <section class="us-card" aria-labelledby="us-t1">
    <header class="us-head">
      <div>
        <h3 id="us-t1">Reply draft</h3>
        <p>Ticket #58214 · Ana Oliveira · Order NW-48213</p>
      </div>
      <span class="mv-badge" data-variant="secondary" data-shape="pill">AI draft</span>
    </header>
    <p class="us-ask"><b>Ana Oliveira · 9:12 AM</b>Hi, my Brewline French Press arrived with a cracked lid. Can I get a refund, and do I have to pay for return shipping?</p>

    <mv-unsourced id="us-reply" label="Reply grounding">
      <script type="application/json" data-sources>[
        { "id": "order", "title": "Order NW-48213", "publisher": "Order history", "date": "2026-09-18", "snippet": "Delivered Sep 18, 2026, 2:14 PM · FedEx Ground · signed by A. Oliveira." },
        { "id": "returns", "title": "Returns & refunds policy", "url": "https://help.example.com/returns", "publisher": "Help Center", "date": "2026-03-04" },
        { "id": "shipping", "title": "Return shipping FAQ", "url": "https://help.example.com/return-shipping", "publisher": "Help Center", "date": "2026-05-12" },
        { "id": "warranty", "title": "Warranty terms", "url": "https://help.example.com/warranty", "publisher": "Help Center", "date": "2025-11-20" }
      ]</script>
      <script type="application/json" data-claims>[
        { "id": "c2", "evidence": [{ "source": "returns", "quote": "Items that arrive damaged or defective can be refunded or replaced free of charge within 30 days of delivery." }] },
        { "id": "c3", "evidence": [
          { "source": "returns", "quote": "For damaged items, we cover return shipping and email a prepaid label within one business day." },
          { "source": "shipping", "quote": "Prepaid return labels are sent by email and work at any FedEx or UPS drop-off point." }
        ] },
        { "id": "c4", "evidence": [{ "source": "returns", "stance": "contradicts", "quote": "Refunds are issued to the original payment method 5 to 10 business days after the carrier scans the return." }] },
        { "id": "c7", "evidence": [{ "source": "warranty", "stance": "partial", "score": 0.35, "quote": "Brewline accessories are covered by our standard limited warranty." }] }
      ]</script>
      <p>Hi Ana, thanks for reaching out, and I'm sorry the lid arrived cracked.</p>
      <p><mark data-claim="c1" data-cites="order">Your order was delivered on September 18</mark>, and <mark data-claim="c2">damaged items can be refunded or replaced free of charge within 30 days of delivery</mark>, so you're well within the window. <mark data-claim="c3">Return shipping is on us: we'll email you a prepaid label</mark>.</p>
      <p><mark data-claim="c4">Once the carrier scans the package, your refund will post within 2 business days</mark>. <mark data-claim="c5">We'll also waive the $12 restocking fee as a courtesy</mark>.</p>
      <p>If you'd rather keep the press, <mark data-claim="c6">replacement lids ship within 24 hours</mark>, and <mark data-claim="c7">the glass carafe is covered by a 2-year warranty</mark>. <mark data-claim="c8" data-support="opinion">A replacement is probably the faster option for you</mark>.</p>
      <p>Best,<br>Jordan, Customer Care</p>
    </mv-unsourced>

    <details id="us-plain">
      <summary>Plain text that will be sent</summary>
      <pre id="us-plain-text"></pre>
    </details>

    <footer class="us-foot">
      <label class="mv-choice">
        <input type="checkbox" role="switch" class="mv-switch" id="us-chips" checked>
        <span class="mv-choice-text"><span class="mv-choice-title">Source numbers</span></span>
      </label>
      <div class="us-actions">
        <span class="us-state" id="us-state" aria-live="polite"></span>
        <button class="mv-button" type="button" data-size="sm" id="us-send">Send reply</button>
      </div>
    </footer>
  </section>

  <!-- 2 · Research summary from data: text + claim spans, references list -->
  <section class="us-card" aria-labelledby="us-t2">
    <header class="us-head">
      <div>
        <h3 id="us-t2">Churn analysis summary</h3>
        <p>Generated from five internal sources · rendered from text and claim spans</p>
      </div>
      <button class="mv-button" type="button" data-variant="outline" data-size="sm" id="us-recheck">Re-check sources</button>
    </header>
    <mv-unsourced id="us-brief" references attention="unsourced contradicted weak" filter-label="Show only claims to review" label="Summary grounding"></mv-unsourced>
  </section>

  <p class="us-hint">Hover or Tab to a claim to see its evidence · <kbd>[</kbd> <kbd>]</kbd> step through the claims that need a source</p>

  <script type="module">
    await customElements.whenDefined("mv-unsourced");
    const $ = (id) => document.getElementById(id);
    const wait = (ms) => new Promise((r) => setTimeout(r, ms));

    /* ── 1 · Reply draft ─────────────────────────────── */
    const reply = $("us-reply");
    const send = $("us-send");
    const state = $("us-state");
    const plain = $("us-plain-text");

    // What the app's retrieval finds (a real app would query its knowledge base here).
    const found = {
      c6: [{ source: { id: "parts", title: "Replacement parts inventory", publisher: "Inventory · live", date: "2026-09-24" }, quote: "Brewline French Press lids: 148 in stock, ship within 24 hours of order." }],
    };
    reply.addEventListener("mv-find-source", (e) => {
      e.detail.waitUntil(wait(1100).then(() => found[e.detail.id] ?? []));
    });

    const paint = () => {
      if (!reply.isConnected) return;
      const s = reply.stats;
      state.textContent = s.attention
        ? `${s.attention} claim${s.attention > 1 ? "s" : ""} to fix before sending`
        : "Every claim is sourced or marked as opinion";
      send.dataset.variant = s.attention ? "outline" : "";
      if (!s.attention) send.removeAttribute("data-variant");
      plain.textContent = reply.toText();
    };
    reply.addEventListener("mv-claim-change", () => queueMicrotask(paint));
    send.addEventListener("click", () => {
      const s = reply.stats;
      if (s.attention) { reply.next(); return; }
      send.textContent = "Sent";
      send.disabled = true;
    });
    $("us-chips").addEventListener("change", (e) => {
      if (e.target.checked) reply.removeAttribute("citations");
      else reply.setAttribute("citations", "hidden");
    });
    requestAnimationFrame(paint);

    /* ── 2 · Summary from data ───────────────────────── */
    const brief = $("us-brief");
    brief.sources = [
      { id: "dashboard", title: "Churn dashboard", url: "https://metrics.example.com/churn", publisher: "Metrics", date: "2026-09-01" },
      { id: "survey", title: "Cancellation exit survey, August", url: "https://wiki.example.com/research/exit-survey-aug", publisher: "Research ops", date: "2026-09-03" },
      { id: "interviews", title: "Customer interviews, round 3 (n=24)", publisher: "Research ops", date: "2026-08" },
      { id: "experiment", title: "Pricing experiment #14 readout", url: "https://wiki.example.com/growth/exp-14", publisher: "Growth team", date: "2026-08-27" },
      { id: "tickets", title: "Support tickets export", publisher: "Support", date: "2026-09-02" },
    ];
    const claims = [
      { id: "b1", match: "Monthly churn rose to 4.1% in August, up from 3.2% in June", evidence: [{ source: "dashboard", quote: "Monthly plan churn: Jun 3.2% · Jul 3.6% · Aug 4.1%." }] },
      { id: "b2", match: "most customers who left named price as the main reason", evidence: [{ source: "survey", quote: "Price was the top reason for cancelling: 58% of 212 responses in August." }] },
      { id: "b3", match: "the July price increase is the likely trigger", evidence: [{ source: "interviews", stance: "partial", score: 0.3, quote: "Several customers mentioned the new price, but half of them had downgraded before July." }] },
      { id: "b4", match: "Annual plans were largely unaffected", evidence: [{ source: "dashboard", quote: "Annual plan churn: Jun 0.9% · Aug 1.0%." }] },
      { id: "b5", match: "Two competitors now bundle onboarding for free" },
      { id: "b6", match: "the $29 tier lifted trial-to-paid conversion by 9%", evidence: [{ source: "experiment", stance: "contradicts", quote: "Trial-to-paid conversion for the $29 tier: +4.2% (not statistically significant)." }] },
      { id: "b7", match: "Billing tickets doubled over the same period", evidence: [{ source: "tickets", quote: "Billing tickets: 312 in June, 655 in August." }] },
      { id: "b8", match: "Reverting monthly pricing before Q4 would recover most of the lost revenue" },
    ];
    brief.text = "Monthly churn rose to 4.1% in August, up from 3.2% in June, and most customers who left named price as the main reason: the July price increase is the likely trigger. Annual plans were largely unaffected. Two competitors now bundle onboarding for free.\n\nIn the pricing experiment, the $29 tier lifted trial-to-paid conversion by 9%. Billing tickets doubled over the same period. Reverting monthly pricing before Q4 would recover most of the lost revenue.";
    brief.claims = claims;

    const teardown = [{ source: { id: "teardown", title: "Competitive teardown, Q3", publisher: "Product marketing", date: "2026-09-10" }, quote: "Two of our three main competitors now include guided onboarding in every paid plan at no extra cost." }];
    brief.addEventListener("mv-find-source", (e) => {
      e.detail.waitUntil(wait(1200).then(() => (e.detail.id === "b5" ? teardown : [])));
    });

    // Re-check: every claim goes back to “checking”, then resolves one by one.
    let run = 0;
    $("us-recheck").addEventListener("click", async () => {
      const id = ++run;
      brief.close();
      brief.claims = claims.map((c) => ({ ...c, support: "pending" }));
      await wait(700);
      for (const c of claims) {
        if (id !== run || !brief.isConnected) return;
        brief.updateClaim(c.id, { support: null });
        await wait(380);
      }
    });
  </script>
</div>

Kulturelle Referenz

Pinocchios Abenteuer, Carlo Collodi (1883, Buch). Jedes Mal, wenn der hölzerne Junge lügt, wächst seine Nase, sodass eine Unwahrheit von selbst sichtbar wird, direkt an dem, der sie ausgesprochen hat, ohne dass jemand sie anprangern muss. Hier lässt jede nicht gestützte Aussage an ihrem Ende einen leisen Schweif wachsen, umso länger, je mehr von ihr auf nichts steht, und er schrumpft zu einem nummerierten Quellen-Chip zurück, sobald ein Beleg angehängt wird.

API

Attributes

NameTypDefaultDescription
attentionspace-separated statusesunsourced contradictedWhich statuses need attention: they are counted in stats.attention, reached by the stepper and the [ ] keys, and stay lit when the filter is on. Add weak for a stricter review.
filterbooleanDims everything except the claims that need attention (ink fades, their text gets an underline in its status color). Reflects the bar's toggle.
filter-labelstringShow only unsourcedText of the filter toggle (e.g. “Show only claims to review” when attention includes weak).
summary"true" | "false"truefalse hides the summary bar (counts, coverage meter, stepper, filter). The [ ] keys and the live announcements keep working.
citations"hidden"Hides the numbered source chips in the text (tails, lens and references are unchanged), for a cleaner reading view.
referencesbooleanAppends a numbered Sources list after the text: title (linked when http/https), publisher, en-US date, and how many claims each source backs or contradicts.
labelstringClaim groundingAccessible name of the summary bar.
data-claimstring (id)On any inline element (<mark> recommended) inside the component: makes it a claim. The value is its id (auto-generated when empty) used by the claims data, events and methods. Nested claims are ignored; claims added later (streaming) are picked up.
data-cites / data-contradictsspace-separated source idsOn a claim: sources that support it / contradict it. The quote defaults to the source's snippet.
data-supportsourced | weak | contradicted | unsourced | opinion | pendingOn a claim: explicit status, overriding the one derived from evidence (claims data with a support key wins over it).
data-quote / data-notestringOn a claim: quote for its first cited source / note shown in the lens instead of the default status sentence.
data-status / data-attention / data-active / data-current / data-searchingset by the componentOn claims: computed status (or removed), needs attention, lens open, reached by the stepper, retrieval running. The host gets data-state="review | clear | empty", data-filter and data-inspecting.

Properties

NameTypDescription
sourcesArray<{ id, title, url?, publisher?, date?, snippet? }>Source library. Also read from a <script type="application/json" data-sources> child. Only http(s) URLs become links (new tab); ISO dates are shown as “Mar 4, 2026”.
claimsArray<{ id, support?, note?, evidence?, start?, end?, match? }>Claim data, merged with the markup by id (also read from a <script type="application/json" data-claims> child). evidence: [{ source: id | {…source}, quote?, stance?: "supports" | "partial" | "contradicts", score?: 0..1 }]. With text set, start/end (UTF-16 offsets) or match (exact substring, searched in order) place each claim. Reading returns a snapshot per claim: { id, index, text, status, label, attention, note, evidence (with number), opinion, removed, searching, element }.
textstring | nullData mode: the component renders this plain text itself (text nodes only, never HTML) into a [data-unsourced-body] container, one <p> per blank-line-separated paragraph, with a <mark data-claim> per claim span. A span never crosses a paragraph break; overlapping spans are dropped.
stats{ total, sourced, weak, contradicted, unsourced, opinion, pending, removed, attention, coverage }Read-only tally. total excludes removed claims; coverage = sourced / (total − opinion − pending), null when nothing is factual.
attention / filter / filterLabel / summary / citations / references / labelreflectedMirror the attributes.

Methods

NameDescription
select(id | element)Moves focus to a claim, scrolls it into view and opens its evidence. Returns false for unknown or removed claims.
next() / previous()Steps to the next / previous claim that needs attention (wrapping), like the stepper and the ] and [ keys. Returns false when none is left.
updateClaim(id, patch)Merges { support, evidence, note } into a claim's data (support: null returns to the derived status), e.g. to resolve a pending check. Emits mv-claim-change when the status changes.
addEvidence(id, evidence | evidence[])Attaches evidence found later (when mv-find-source was handled without waitUntil). A supporting source makes an unsourced or weak claim sourced; the tail retracts into a chip.
removeClaim(id) / restoreClaim(id) / markOpinion(id, on = true)Programmatic versions of the lens actions. Return false when nothing changed or the change was canceled.
toText()The current text as plain text (blank line between blocks), without removed claims and without the component's own marks; punctuation orphaned by a removed sentence is dropped.
close()Closes the evidence lens.

Events

NameDescription
mv-claim-selectA claim was opened by click, keyboard focus, a chip, the stepper or select() (not by hover). detail: the claim snapshot plus trigger: "click" | "focus" | "chip" | "step" | "api".
mv-find-sourceFind a source was pressed. Cancelable (preventDefault: nothing happens). detail: { id, text, claim, waitUntil(promise) }. Resolve the promise with evidence (an array, { evidence }, or a single item; inline source objects are added to the library) and it is attached; resolve with nothing for “No source found”, reject for “The search failed”. Without waitUntil the lens says “Search requested” and the app calls addEvidence() later.
mv-claim-changeA claim's status is changing. detail: { id, action: "remove" | "restore" | "opinion" | "claim" | "evidence" | "update", from, to, user, claim }. Cancelable when the user pressed Remove claim, Undo, Mark as opinion or Treat as a claim (user: true): preventDefault() vetoes it, e.g. a claim that legal requires.

Content structure

NameDescription
(content)Any text (paragraphs, lists, headings) containing [data-claim] elements, plus optional JSON <script data-sources> / <script data-claims> children. The summary bar is prepended; the Sources list, the lens (a popover) and a visually hidden live region are appended. Claim text is moved into a .mv-unsourced-text wrapper, never rewritten.

CSS classes

NameDescription
mv-unsourced-claimEvery claim (data-status, data-attention…). Inside: .mv-unsourced-text (the author's text), .mv-unsourced-marks (aria-hidden: .mv-unsourced-chips > .mv-unsourced-chip[data-n][data-stance] and .mv-unsourced-tail > .mv-unsourced-tail-line + .mv-unsourced-tail-label) and a visually hidden status suffix.
mv-unsourced-stub“Claim removed · Undo” button inserted after a removed claim.
mv-unsourced-barSummary bar: .mv-unsourced-counts (per-status .mv-unsourced-count with a .mv-unsourced-glyph), .mv-unsourced-meter (.mv-unsourced-seg[data-status]), .mv-unsourced-stepper (.mv-unsourced-step, .mv-unsourced-pos) and .mv-unsourced-filter[aria-pressed].
mv-unsourced-lensEvidence popover (data-status): .mv-unsourced-lens-head, -note, .mv-unsourced-evidence > .mv-unsourced-ev[data-stance] (number, title link, .mv-unsourced-stance, meta, .mv-unsourced-quote with .mv-unsourced-hit words), .mv-unsourced-lens-find and .mv-unsourced-actions (.mv-button).
mv-unsourced-refsOptional numbered Sources list (.mv-unsourced-ref-list > .mv-unsourced-ref).

CSS variables

NameDefaultDescription
--mv-unsourced-colorwarning mixed with fgTone of unsourced claims: tail, tag, meter segment, active filter.
--mv-unsourced-contradicteddanger mixed with fgTone of contradicted claims and contradicting evidence.
--mv-unsourced-weakwarning mixed with fg-mutedTone of weakly supported claims (dotted tail, dashed chips).
--mv-unsourced-groundedvar(--mv-accent)Tone of sourced claims (chip hover, meter segment).
--mv-unsourced-tail-min / --mv-unsourced-tail-max0.75em / 2.75emShortest and longest tail; the length in between follows --_g, set per claim (0..1, output).
--mv-unsourced-leading1.75Line height of the text (leaves room for the chips and the status tag).

Accessibility

Status never relies on color: each claim is a keyboard stop (role=button, aria-haspopup="dialog", aria-expanded) whose accessible name is its own text plus a visually hidden suffix (“(no source)”, “(sourced, 2 sources)”, “(weak support, 1 source)”, “(contradicted by 1 source)”, “(marked as opinion)”, “(checking sources)”), while the chips and tails are aria-hidden and drawn with distinct shapes (numbered chip, open ring, stop bar, dotted line, dashed line). Keyboard focus opens the evidence without moving focus; Enter or Space moves into it (source links, then the actions), Tab past its last control resumes reading right after the claim, Shift+Tab goes back to the claim and Escape closes it and returns focus. Arrow keys, Home and End move between claims; ] and [ jump to the next and previous claim that needs a source from anywhere inside the component (never while typing in a field), as do the stepper buttons (aria-keyshortcuts, “2 of 3”). The filter is a real toggle button (aria-pressed). The lens is a non-modal role="dialog" labelled by its status line, placed with a manual popover and also closed by an outside click; touch taps open and close it. Removing a claim moves focus to its “Claim removed · Undo” button, whose name quotes the removed text, and Undo returns focus to the claim. A polite live region announces each action and search result followed by the new tally (“Source attached: Replacement parts inventory. Claim is now sourced. 8 claims · 4 sourced · …”), debounced, and never on page load. Source links open in a new tab and say so. Reduced motion (OS setting or data-motion="reduce"): tails and chips appear and retract instantly, the checking pulse stops. Forced colors: tails, chips and tags use CanvasText with borders, highlighted quote words use Mark, focus uses Highlight.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden