익스클루시브

Odd One Out <mv-odd-one-out>

서로 같아야 하는 항목 집합에서 이상값을 자동으로 찾아냅니다. 서버, 컨테이너, 환경 간 설정 편차, 상품 변형 간 가격 및 재고 불일치, 같은 역할의 동료와 권한이 다른 직원 한 명, 설정이 어긋난 리전 하나 등을 찾아냅니다.

문화적 레퍼런스

세서미 스트리트, Children's Television Workshop (조앤 갠즈 쿠니, 로이드 모리셋) (1969, 시리즈). 반복되는 코너에서 네 가지가 나란히 제시되고, 시청자는 정답이 공개되기 전에 나머지와 다른 하나를 찾아내야 합니다. UI에서는 동일해야 하는 항목 집합을 필드별로 그 집합의 다수값과 비교하고, 패턴을 깨는 값을 제자리에서 짚어 개수를 세고 설명하며, 다시 맞출 수 있는 방법을 제공합니다.

작동 방식

서로 같아야 하는 항목 집합에서 이상값을 자동으로 찾아냅니다: 서버, 컨테이너, 환경 간 설정 드리프트, 한 제품의 여러 변형 간 가격과 재고 불일치, 같은 역할의 동료들과 권한이 다른 직원 한 명, 설정이 다른 리전 하나. 직접 만든 카드나 표 행을 감싸거나(항목에는 data-item, 값에는 data-field="key") items 배열을 전달하면 표(행 또는 열마다 항목 하나) 또는 카드로 렌더링합니다. 각 필드마다 다수값(정확히 일치, 대소문자와 악센트 무시, 또는 직접 지정한 normalize나 comparator)을 찾고, 숫자는 강건한 MAD 검정 또는 절대값 / % 허용 오차와 함께 중앙값을 구한 뒤, 튀는 값을 제자리에 표시합니다: 링, 색이 입혀진 셀, 그리고 어떻게 다른지 기호로 알려 주는 배지(≠ 다름, ▲ 높음, ▼ 낮음, ∅ 누락, + 여기에만 있음)와 “differs from 7 of 8”. 모든 항목이 서로 다른 필드(호스트 이름, IP)는 그대로 두고, 뚜렷한 다수값이 없는 필드는 따로 나열하며, 다른 항목 대부분에 있는 필드가 빠진 항목도 표시합니다. 요약은 각 이상 항목을 나열하고(“web-04 differs on 3 fields: TLS min version, Max body size, Region”) 이상값을 다수값 옆에 보여 주며, 바로가기 링크, 이상값 사이를 오가는 N / P 탐색, 그리고 취소 가능한 mv-align 이벤트를 거쳐 앱이 수정을 적용하도록 하는 값별 “Align” 동작(선택 사항)을 제공합니다. “Differences only” 스위치는 일치하는 모든 것을 흐리게 합니다(렌더링된 보기에서는 일치하는 필드도 접습니다). 마크업, 입력, 데이터가 바뀌면 재검사가 실시간으로 실행되며 정중하게(polite) 알립니다.

카테고리데이터 표시
유형Web Component (<mv-odd-one-out>)
상태안정
키트알림 피로 없는 모니터링
함께 설치되는 항목button, checkbox, switch
Keywordsexclusive, culture, outlier, anomaly, drift, config-drift, consistency, majority, median, mad, compare, diff, audit, access-review, permissions, variants, inventory, fleet, devops, data-quality

When to use

  • A fleet of servers, containers or feature-flag environments must share one configuration and drift has to jump out
  • A product's variants (sizes, colors, bundles) should share price, weight or tax codes except for the one mistyped
  • An access review compares people in the same role and must surface the one with an extra or missing permission
  • A small set of regions, stores or tenants should be configured alike and a reviewer wants the exceptions first

Avoid when

  • Two or three documents of different kinds must be reconciled field by field against a reference, with a verdict → use Cross-Check instead
  • Several sources describe the same event over time and the disagreements are about what happened when → use Accounts instead
  • Thousands of records are involved: compute outliers server-side and show them in a paginated table → use Data table instead

설치

node scripts/add.mjs odd-one-out --out ./src/marvelous

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

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, components/switch/switch.css, core/dom.js, core/element.js, core/motion.js, components/odd-one-out/odd-one-out.js, components/odd-one-out/odd-one-out.css.

사용법

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

<mv-odd-one-out item-key="host">
  <p data-item="web-01"><b data-field="host">web-01</b> <span data-field="tls">TLS 1.3</span></p>
  <p data-item="web-02"><b data-field="host">web-02</b> <span data-field="tls">TLS 1.2</span></p>
  <p data-item="web-03"><b data-field="host">web-03</b> <span data-field="tls">TLS 1.3</span></p>
</mv-odd-one-out>

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

<div id="ooo-demo" style="width:min(100%,74rem);margin-inline:auto">
  <style>
    #ooo-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1rem }
    #ooo-demo .ooo-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap }
    #ooo-demo .ooo-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #ooo-demo .ooo-top p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ooo-demo .ooo-panel[hidden] { display:none }
    #ooo-demo .ooo-fleet { overflow:auto; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #ooo-demo .ooo-fleet table { width:100%; border-collapse:separate; border-spacing:0; font-size:.8125rem; font-variant-numeric:tabular-nums }
    #ooo-demo .ooo-fleet th, #ooo-demo .ooo-fleet td { padding:.5625rem .75rem; border-bottom:1px solid var(--mv-border); text-align:start; white-space:nowrap }
    #ooo-demo .ooo-fleet tbody tr:last-child > * { border-bottom:0 }
    #ooo-demo .ooo-fleet thead th { background:var(--mv-bg-subtle); color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase }
    #ooo-demo .ooo-fleet td:first-child { font-family:var(--mv-font-mono); font-weight:600 }
    #ooo-demo .ooo-fleet .ooo-dot { display:inline-block; width:.5rem; height:.5rem; margin-inline-end:.5rem; border-radius:50%; background:var(--mv-success); vertical-align:.05em }
    #ooo-demo .ooo-controls { display:flex; align-items:center; gap:.75rem 1rem; flex-wrap:wrap; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
    #ooo-demo .ooo-controls p { flex:1 1 18rem; margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ooo-demo .ooo-log { flex:1 1 100%; min-height:1.1rem; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
  </style>

  <div class="ooo-top">
    <div>
      <h3 id="ooo-title">Production web tier · 8 hosts</h3>
      <p id="ooo-sub">Every host should run the same edge configuration. Press N / P to walk through what differs.</p>
    </div>
    <mv-segmented id="ooo-scenario" value="fleet" label="Scenario">
      <button value="fleet">Server fleet</button>
      <button value="variants">Product variants</button>
      <button value="access">Access review</button>
    </mv-segmented>
  </div>

  <!-- Markup mode: the app's own table, rows marked with data-item, cells with data-field -->
  <section class="ooo-panel" data-panel="fleet">
    <mv-odd-one-out id="ooo-fleet" item-key="host" alignable label="Production web tier">
      <div class="ooo-fleet">
        <table>
          <thead>
            <tr>
              <th data-field="host">Host</th><th data-field="image">Image</th><th data-field="tls">TLS min</th><th data-field="body">Max body</th>
              <th data-field="workers">Workers</th><th data-field="keepalive">Keepalive</th><th data-field="gzip">Gzip</th><th data-field="region">Region</th><th data-field="p95">p95 latency</th>
            </tr>
          </thead>
          <tbody>
            <tr data-item="web-01"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-01</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">12 ms</td></tr>
            <tr data-item="web-02"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-02</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive" id="ooo-keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">14 ms</td></tr>
            <tr data-item="web-03"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-03</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">11 ms</td></tr>
            <tr data-item="web-04"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-04</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.2</td><td data-field="body">1m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-west-2</td><td data-field="p95">13 ms</td></tr>
            <tr data-item="web-05"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-05</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">12 ms</td></tr>
            <tr data-item="web-06"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-06</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">15 ms</td></tr>
            <tr data-item="web-07"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-07</td><td data-field="image">nginx 1.27.1</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">48 ms</td></tr>
            <tr data-item="web-08"><td data-field="host"><span class="ooo-dot" aria-hidden="true"></span>web-08</td><td data-field="image">nginx 1.27.2</td><td data-field="tls">TLS 1.3</td><td data-field="body">10m</td><td data-field="workers">4096</td><td data-field="keepalive">65s</td><td data-field="gzip">on</td><td data-field="region">us-east-1</td><td data-field="p95">13 ms</td></tr>
          </tbody>
        </table>
      </div>
    </mv-odd-one-out>
  </section>

  <!-- Data mode, cards: one product's variants -->
  <section class="ooo-panel" data-panel="variants" hidden>
    <mv-odd-one-out id="ooo-variants" item-key="variant" layout="cards" alignable label="Ridge Shell Jacket variants"></mv-odd-one-out>
  </section>

  <!-- Data mode, one column per person: peers in the same role -->
  <section class="ooo-panel" data-panel="access" hidden>
    <mv-odd-one-out id="ooo-access" item-key="name" layout="columns" alignable label="Support engineer, tier 2 permissions"></mv-odd-one-out>
  </section>

  <div class="ooo-controls">
    <p id="ooo-hint">Align asks the app to apply the majority value; the check re-runs on its own when the table changes.</p>
    <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="ooo-drift">Simulate a config push on web-02</button>
    <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="ooo-reset">Reset</button>
    <p class="ooo-log" id="ooo-log" aria-live="off"></p>
  </div>

  <script type="module">
    const root = document.getElementById("ooo-demo");
    const fleet = document.getElementById("ooo-fleet");
    const variants = document.getElementById("ooo-variants");
    const access = document.getElementById("ooo-access");
    const log = document.getElementById("ooo-log");
    const say = (text) => { log.textContent = `${new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" })} · ${text}`; };

    const fleetMarkup = fleet.querySelector("tbody").cloneNode(true);

    const variantData = () => [
      { variant: "Slate · XS", sku: "RSJ-SL-XS", price: "$189.00", weight: "310 g", material: "Recycled nylon", hs: "6201.40", rating: "20,000 mm", care: "Machine wash 30°C" },
      { variant: "Slate · S", sku: "RSJ-SL-S", price: "$189.00", weight: "325 g", material: "Recycled nylon", hs: "6201.40", rating: "20,000 mm", care: "Machine wash 30°C" },
      { variant: "Slate · M", sku: "RSJ-SL-M", price: "$189.00", weight: "340 g", material: "Recycled Nylon", hs: "6201.40", rating: "20,000 mm", care: "Machine wash 30°C" },
      { variant: "Slate · L", sku: "RSJ-SL-L", price: "$18.90", weight: "355 g", material: "Recycled nylon", hs: "6201.40", rating: "20,000 mm", care: "Machine wash 30°C" },
      { variant: "Slate · XL", sku: "RSJ-SL-XL", price: "$189.00", weight: "370 g", material: "Recycled nylon", hs: "6202.40", rating: "20,000 mm", care: "Machine wash 30°C" },
      { variant: "Slate · XXL", sku: "RSJ-SL-XXL", price: "$189.00", weight: "385 g", material: "Recycled nylon", hs: "6201.40", rating: "20,000 mm" },
    ];
    const variantFields = [
      { key: "sku", label: "SKU" },
      { key: "price", label: "Price" },
      { key: "weight", label: "Weight" },
      { key: "material", label: "Shell", compare: "fold" },
      { key: "hs", label: "HS code", compare: "exact" },
      { key: "rating", label: "Waterproofing" },
      { key: "care", label: "Care label" },
    ];

    const accessData = () => [
      { name: "Aisha Khan", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "No", export: "No", logs: "Read", sso: "support-t2", mfa: "Hardware key" },
      { name: "Mateo Rossi", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "No", export: "No", logs: "Read", sso: "support-t3", mfa: "Hardware key" },
      { name: "Yuki Tanaka", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "No", export: "No", logs: "Read", sso: "support-t2", mfa: "SMS code" },
      { name: "Lars Nilsson", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "Yes", export: "No", logs: "Read", sso: "support-t2", mfa: "Hardware key" },
      { name: "Priya Raman", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "No", export: "No", logs: "Read", sso: "support-t2", mfa: "Hardware key" },
      { name: "Tomás Silva", tickets: "Read & write", pii: "Masked", refunds: "Up to $500", billing: "No", export: "No", logs: "Read", sso: "support-t2", mfa: "Hardware key" },
    ];
    const accessFields = [
      { key: "name", label: "Name" },
      { key: "tickets", label: "Tickets" },
      { key: "pii", label: "Customer PII" },
      { key: "refunds", label: "Refunds" },
      { key: "billing", label: "Billing admin" },
      { key: "export", label: "Data export" },
      { key: "logs", label: "Production logs" },
      { key: "sso", label: "SSO group" },
      { key: "mfa", label: "MFA method" },
    ];

    variants.fields = variantFields;
    variants.items = variantData();
    access.fields = accessFields;
    access.items = accessData();

    const copy = {
      fleet: ["Production web tier · 8 hosts", "Every host should run the same edge configuration. Press N / P to walk through what differs."],
      variants: ["Ridge Shell Jacket · Slate · 6 sizes", "Sizes share price, codes and labels; weight varies by size and is judged against the median, not exact equality."],
      access: ["Support engineer, tier 2 · 6 people", "Same role, same access expected. One column per person, one row per permission."],
    };
    document.getElementById("ooo-scenario").addEventListener("mv-change", (e) => {
      const v = e.detail.value;
      for (const p of root.querySelectorAll(".ooo-panel")) p.hidden = p.dataset.panel !== v;
      document.getElementById("ooo-title").textContent = copy[v][0];
      document.getElementById("ooo-sub").textContent = copy[v][1];
      document.getElementById("ooo-drift").hidden = v !== "fleet";
      log.textContent = "";
    });

    for (const el of [fleet, variants, access]) {
      el.addEventListener("mv-oddities", (e) => {
        const { count, items } = e.detail;
        say(count ? `mv-oddities · ${count} odd value${count === 1 ? "" : "s"} in ${items.join(", ")}` : "mv-oddities · everything agrees");
      });
      el.addEventListener("mv-align", (e) => {
        const d = e.detail;
        say(`mv-align · ${d.itemLabel} ${d.fieldLabel}: ${d.value} → ${d.majority ?? "(removed)"}`);
      });
    }

    document.getElementById("ooo-drift").addEventListener("click", () => {
      document.getElementById("ooo-keepalive").textContent = "75s";
    });
    document.getElementById("ooo-reset").addEventListener("click", () => {
      fleet.querySelector("tbody").replaceWith(fleetMarkup.cloneNode(true));
      variants.items = variantData();
      access.items = accessData();
      say("Reset.");
    });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
item-keystring (field key)Field that names each item (host, sku, email). It is used as the item label and never compared. In data mode it is also the id property (default "id", then "name").
compareauto | exact | fold | number | number:<tol> | number:<tol>% | ignoreautoDefault rule for fields without their own (data-compare on a header or a field, or compare in the fields property). auto treats a field as numeric when every value parses as a number with the same prefix and unit ($, ms, GB), otherwise exact. exact compares trimmed text with collapsed spaces; fold ignores case and accents; number without tolerance flags robust outliers (modified z-score over the median absolute deviation, or any value off the common one when most items are identical); number:0.5 or number:5% flags values farther than that from the median; ignore never compares. Set exact on numeric-looking codes (HS codes, ZIPs, versions you do not want ranked) so they are reported as ≠ rather than ▲ / ▼.
sensitivitynumber3.5Modified z-score above which a numeric value is an outlier (Iglewicz and Hoaglin's 3.5). Lower flags more.
quorumnumber (0..1)0.5Share of items the majority must exceed. Below it the field has no clear majority: nothing is marked in place and the field is listed under “No clear majority”. A field where every item differs (hostnames, IPs) is treated as naturally unique and left alone.
min-itemsnumber3Fewest items needed to speak of a majority. With fewer, the summary says so and nothing is marked.
differences-onlybooleanDims every agreeing value (and agreeing items further) so the odd ones pop; rendered views also hide fields where every item agrees. Reflected by the summary's switch.
alignablebooleanAdds an “Align” button to each odd value in the summary. It emits mv-align (cancelable); see the event for the default action.
summarytop | bottom | nonetopWhere the summary panel goes, or none to only mark values in place (events, navigation methods and announcements still work).
badgesauto | full | compact | noneautoIn-place badge text: full (“differs from 7 of 8”), compact (“7/8”, full text in the tooltip and for screen readers) or none (ring only). auto is compact inside table cells, full elsewhere.
layoutrows | columns | cardsrowsData mode only: a table with one item per row, a table with one item per column (best for many fields and few items, like environments or people), or a grid of cards.
labelstringConsistency checkAccessible name of the rendered table and its scroll region.
data-itemstring (on an item)Marks an item (card, row, section) and gives its id. Without any, direct children containing [data-field] are the items. data-item-label or a [data-item-title] child gives a nicer label.
data-fieldstring (on a value)Key of a value inside an item. The value is data-value when present, the value of an input / select / textarea (checked state for checkboxes), otherwise the text (the component's own badges are skipped). An element with data-field outside any item is the field's header: its text is the field label and it receives an odd-count pill.
data-compare / data-field-labelstring (on a header or value)Per-field rule (same syntax as compare) and label.
data-odd-one-out / -kind / -item / -current / -countset by the componentOn values: odd | agree | split | neutral, and the kind (differs, higher, lower, missing, extra). On items: odd | agree. data-odd-one-out-current on the oddity being visited; data-odd-one-out-count on headers. The host gets data-state="odd | clear | few".

Properties

Name유형Description
itemsobject[] | nullData mode: one object per item; the component renders the view set by layout. null (default) reads the markup. Objects are copied; align changes the copies.
fields(string | { key, label?, compare?, format?, normalize? })[] | { [key]: { … } }Field order, labels and rules. compare also accepts a function (a, b) => boolean; normalize(value) returns the text actually compared; format(value) the text displayed (data mode). Applies to markup mode too (labels and rules).
odditiesobject[]Read-only: { id, item, itemLabel, element, field, fieldLabel, fieldElement, value, majority, agree, total, kind, basis ("majority" or "median"), badge, description } in item then field order.
splits{ field, label, groups: { value, count }[] }[]Read-only: fields without a clear majority.
report{ field, label, status, majority, agree, total, rule }[]Read-only verdict per field; status is agree, odd, split, unique, ignored or skipped.
stringsobjectEvery visible and announced text (en-US by default), merged with the defaults. Plural entries are [one, other] arrays.

Methods

NameDescription
next() / previous()Moves to the next or previous oddity (wrapping): focuses it, scrolls it into view, marks it current and announces it. Returns false when there is none.
focusOddity(index)Visits a given oddity.
align(item, field)Same as the Align button: emits mv-align and applies the default action unless canceled. Returns true if not canceled.
refresh()Re-checks now (changes are otherwise picked up automatically, debounced).

Events

NameDescription
mv-odditiesAfter the first check and whenever the oddities change. detail: { list (as the oddities property), items (odd item ids), fields (odd field keys), splits, count, total }.
mv-alignAlign was requested for an odd value. Cancelable. detail: { item, itemLabel, field, fieldLabel, value, majority (the target value; null for kind "extra", meaning remove), kind, element, fieldElement, data (the item object in data mode) }. Default action: data mode updates the item's copy and re-renders; markup mode sets the value of an input / select / textarea (firing input and change) or the element's text and data-value. Call preventDefault() to apply it yourself (framework state, API call); the component re-checks when your DOM or data changes.

Content structure

NameDescription
(content)Your items: a table with data-item rows, a grid of cards, sections… Never moved or rebuilt; the component only adds data-odd-one-out-* attributes, tabindex="-1" on a visited value, and small badges marked .mv-odd-one-out-own.

CSS classes

NameDescription
mv-odd-one-out-summarySummary panel (title, Differences only switch, previous / next, list of odd items, No clear majority line); data-state="odd | clear | few".
mv-odd-one-out-badgeIn-place badge appended inside an odd value (after it for inputs); data-kind; holds the glyph, the visible count and a screen-reader description.
mv-odd-one-out-countOdd-count pill appended to a field header.
mv-odd-one-out-view / -table / -cards / -cardViews rendered in data mode.
mv-odd-one-out-ownEvery node the component creates; ignored when values are read.

CSS variables

NameDefaultDescription
--mv-odd-one-out-colorvar(--mv-warning)Tint of odd values, badges and item edges.
--mv-odd-one-out-inkcolor-mix(color, --mv-fg 48%)Text color on odd tints (kept readable in light and dark).
--mv-odd-one-out-agree-colorvar(--mv-success)Icon tint when everything agrees.
--mv-odd-one-out-dim0.4Opacity of agreeing values under Differences only.
--mv-odd-one-out-list-height22remMax height of the summary list before it scrolls.

Accessibility

Color is never the only signal: every odd value gets a ring and a badge whose glyph encodes the kind (≠ differs, ▲ higher, ▼ lower, ∅ missing, + only here), and the badge carries a screen-reader description read with the value itself (“TLS 1.2. Differs from 7 of 8: TLS 1.2, majority TLS 1.3.”); for inputs the description is linked with aria-describedby instead. Field headers get an odd-count pill with a spoken equivalent. The summary is a labelled section whose heading states the result; odd items are a real list, each with a button jumping to the item and one per odd value whose accessible name includes the full description, plus labelled Align buttons (“Align TLS min version on web-04 to TLS 1.3”). N and P move to the next and previous oddity anywhere inside the component (ignored while typing in a text field or with modifiers; buttons with aria-keyshortcuts do the same): the value is focused (tabindex="-1" is added only while it is odd), scrolled into view, marked current with a thicker ring and announced (“Oddity 2 of 5: web-04, Region. Differs from 7 of 8…”). Changes after the first check are announced once in a polite live region with the new summary, and focus stays on the equivalent summary control after a re-render. The Differences only switch is a native checkbox with role="switch". The rendered table has real th headers with scope and a focusable, labelled scroll region. Reduced motion (prefers-reduced-motion or data-motion="reduce"): the one-shot highlight pulse and badge nudge are removed and scrolling is instant. Forced colors: odd values are outlined with the Mark system color and the current one with Highlight.

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