익스클루시브

Unsourced <mv-unsourced>

생성된 텍스트나 긴 글(AI 답변, 고객 지원 답변, 리서치 요약, 보고서)을 위한 주장 단위 근거 표시입니다. 모든 주장이 제자리에서 각각 근거가 있는지 보여 줍니다.

문화적 레퍼런스

피노키오의 모험, 카를로 콜로디 (1883, 도서). 나무 소년은 거짓말을 할 때마다 코가 길어지므로, 누가 지적하지 않아도 거짓이 말한 사람에게서 바로 드러납니다. 여기서는 근거 없는 주장마다 끝에 조용한 꼬리가 자라나며, 근거 없는 부분이 많을수록 길어지고, 증거가 붙는 즉시 번호가 매겨진 출처 칩으로 줄어듭니다.

작동 방식

생성된 텍스트나 긴 텍스트(AI 답변, 고객 지원 답변, 리서치 요약, 보고서)를 위한 주장 단위 근거 표시입니다. 각 주장이 제자리에서 스스로, 뒷받침하는 근거가 있는지 보여 줍니다. 주장이 <mark data-claim>로 표시된 텍스트를 감싸고(또는 텍스트와 함께 오프셋이나 정확한 일치로 지정한 주장 범위를 전달합니다. 그라운딩 API가 반환하는 형식입니다) 출처 라이브러리를 제공하면, 각 주장은 sourced, weak, contradicted, unsourced, opinion 또는 checking 상태가 되며, 이는 증거(출처, 인용, 입장 supports | partial | contradicts, 점수)에서 도출되거나 명시적으로 설정됩니다. 근거가 있는 주장에는 각주처럼 번호가 매겨진 작은 출처 칩이 붙고, 근거 없는 주장은 마지막 단어에서 조용한 가는 꼬리가 자라나며 근거 없는 부분이 많을수록 길어집니다(주장 길이 × 부족한 뒷받침). 상태마다 모양이 다르고(열린 원 = 출처 없음, 정지 막대 = 반박됨, 점선 = 약함), 호버나 포커스 시 “No source” 태그가 표시되며, 출처가 붙는 순간 꼬리는 칩으로 줄어듭니다. 호버, 포커스 또는 칩을 누르면 증거가 열립니다. 각 출처와 일치하는 인용이 표시되고 주장과 공유하는 단어가 강조됩니다. 근거 없는 주장에는 Find a source(취소 가능한 mv-find-source로, waitUntil(promise)를 통해 앱이 검색을 실행하고 찾은 결과를 연결), Mark as opinion, Remove claim(제자리에서 실행 취소 가능하며, toText()는 해당 주장을 뺀 텍스트를 반환)을 제공합니다. 요약 바에는 커버리지 미터 위에 “8 claims · 3 sourced · 1 weak · 2 unsourced”가 표시되고, 출처가 필요한 주장을 차례로 이동하며([ 키와 ] 키) 나머지를 흐리게 할 수 있습니다. 선택적으로 번호가 매겨진 Sources 목록으로 텍스트를 마무리합니다.

카테고리데이터 표시
유형Web Component (<mv-unsourced>)
상태안정
키트검증할 수 있는 AI
함께 설치되는 항목button
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

설치

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

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["unsourced"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/unsourced/unsourced.js, components/unsourced/unsourced.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

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

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

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

API

Attributes

Name유형DefaultDescription
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

Name유형Description
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: claims keep CanvasText on Canvas, tails, chips and tags use CanvasText with borders, highlighted quote words use Mark, focus uses Highlight. Known limits: the component checks nothing by itself; support comes from the sources and data-cites / data-contradicts links you provide (or your mv-find-source handler), and the quote highlight matches words, not meaning. Visible texts are English only for now (no strings property yet).

이 페이지는 AI로 번역되었습니다. 번역 문제 신고