独家

Trio Check <mv-trio-check>

用于测验、知识检查和配对练习的批量校验,让猜测毫无用处:答案可自由填写(你自己的标记了 data-trio-item 的输入框、下拉框、单选或复选框,或由 items 属性渲染出的、含文本框、下拉框和选择标签的行),而组件从不透露单个答案是否正确。

文化出处

《奥伯拉·丁的回归》,卢卡斯·波普(2018 年,游戏)。保险账簿只会成批确认你的推理:只有同时答对三个人的命运时,它们才会被锁定为正确,因此无法逐个暴力尝试。在界面中,答案只会以三个一组(或任意批量大小)得到确认并一起封存,而一次失败的尝试不会透露哪个答案错了。

工作原理

面向测验、知识检查和配对练习的批量验证,让猜测毫无用处:答案可自由填写(你自己标记了 data-trio-item 的输入框、下拉框、单选框或复选框,或由 items 属性渲染出的行,包含文本框、下拉框和选项标签),组件从不透露单个答案是否正确。只有当一整组答案(默认 3 个,batch)同时正确时才会被锁定:这些行变为只读并盖上带有“Confirmed”的印章,进度条填满一格,其余答案仍保持不确定(答对 4 个只锁定 3 个;最后一组缩减为剩余数量)。失败的尝试只会显示“Not enough correct yet. Nothing was locked.”,并让进度条轻轻抖动;完全相同的重复提交会被拒绝且不消耗尝试次数;可选的冷却时间会在下次尝试前逐渐耗尽 Verify 按钮。正确性来自异步回调 check(answers)(你的服务器,可以应用同样的规则,只返回它确认的 id),或来自 data-answer-hash 中加盐的 SHA-256 哈希(附带静态 hash() 辅助函数,并忽略大小写、空格和重音差异),因此答案从不会以明文形式发送到浏览器。吸顶栏显示“6 of 12 confirmed · 2 answered”,验证可以在变更时自动运行,进度可以从服务器恢复,完成状态(大印章、尝试次数、你自己的继续按钮)为练习收尾。

分类表单
类型Web Component(<mv-trio-check>)
状态稳定版
同时安装button, input, select
Keywordsexclusive, culture, quiz, assessment, knowledge-check, compliance-training, onboarding, matching, batch-verification, anti-guessing, language-learning, puzzle, form, answers, hash, cooldown, progress, seal

When to use

  • A compliance or onboarding knowledge check must show real understanding, not answers found by trial and error one field at a time
  • A matching exercise pairs terms with definitions, tickets with actions or people with roles, and each pair should not be checkable alone
  • A language or vocabulary drill checks typed answers in the browser without shipping them in clear
  • A puzzle hunt or escape game confirms deductions in sets so players have to reason rather than brute-force

Avoid when

  • Each answer should get immediate right or wrong feedback, as in a practice drill or a form validation → use Field instead
  • The questions are a guided sequence where each step depends on the previous answer → use Stepper instead
  • A graded, high-stakes exam: client-side hashes of short or multiple-choice answers can be brute-forced, so check on the server only

安装

node scripts/add.mjs trio-check --out ./src/marvelous

使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["trio-check"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

复制的文件(含依赖):tokens/tokens.css, core/base.css, components/button/button.css, components/input/input.css, components/select/select.css, core/dom.js, core/element.js, core/motion.js, components/trio-check/trio-check.js, components/trio-check/trio-check.css。

用法

标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:

<div id="tc-demo" style="width:min(100%,66rem);margin-inline:auto">
  <style>
    #tc-demo { display:grid; gap:1.5rem }
    #tc-demo .tc-card { border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #tc-demo .tc-head { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1.125rem 1.25rem 0 }
    #tc-demo .tc-head h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #tc-demo .tc-head p { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.45 }
    #tc-demo .tc-body { display:grid; grid-template-columns:17.5rem minmax(0,1fr); gap:1.25rem; align-items:start; padding:1.125rem 1.25rem 1.25rem }
    #tc-demo .tc-policy { position:sticky; top:1rem; padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); font-size:.8125rem; line-height:1.5 }
    #tc-demo .tc-policy h4 { display:flex; align-items:center; gap:.5rem; margin:0 0 .125rem; font-size:.8125rem }
    #tc-demo .tc-policy h4 svg { width:1rem; height:1rem; color:var(--mv-fg-muted) }
    #tc-demo .tc-policy > p { margin:0 0 .75rem; color:var(--mv-fg-subtle); font-size:.75rem }
    #tc-demo .tc-policy ol { display:grid; gap:.625rem; margin:0; padding:0 0 0 1.125rem }
    #tc-demo .tc-policy li::marker { color:var(--mv-fg-subtle); font-variant-numeric:tabular-nums }
    #tc-demo .tc-policy b { font-weight:600 }
    #tc-demo .tc-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1.25rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle); border-radius:0 0 var(--mv-radius-xl) var(--mv-radius-xl) }
    #tc-demo .tc-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); flex:1 1 18rem }
    #tc-demo .tc-actions { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
    #tc-demo .mv-choice { font-size:.8125rem }
    #tc-demo .tc-lang { padding:1.125rem 1.25rem 1.25rem; display:grid; gap:1rem }
    #tc-demo .tc-lang .tc-head { padding:0 }
    @media (max-width:52rem) {
      #tc-demo .tc-body { grid-template-columns:minmax(0,1fr) }
      #tc-demo .tc-policy { position:static }
    }
  </style>

  <!-- Compliance training: a matching exercise checked by a (fake) server that only knows salted hashes -->
  <section class="tc-card" aria-labelledby="tc-desk-title">
    <div class="tc-head">
      <div>
        <h3 id="tc-desk-title">Refund desk certification</h3>
        <p>Pick the right action for each ticket using only the handbook. Guessing one at a time won’t help.</p>
      </div>
      <span class="mv-badge" data-variant="secondary" data-shape="pill">Module 2 of 4</span>
    </div>
    <div class="tc-body">
      <aside class="tc-policy" aria-labelledby="tc-policy-title">
        <h4 id="tc-policy-title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h12v18H6a2 2 0 0 1-2-2z"/><path d="M8 7h6M8 11h6"/></svg>Returns handbook</h4>
        <p>Tidewell Outfitters · Support policy v3.2</p>
        <ol>
          <li>Unworn items returned <b>within 30 days</b> of delivery: refund to the original payment method.</li>
          <li>Returns between <b>31 and 60 days</b>: store credit only.</li>
          <li>After <b>60 days</b>, or any <b>final-sale</b> item: decline.</li>
          <li>Damaged or wrong item <b>with a photo</b>: send a free replacement, no return needed.</li>
          <li>Any refund <b>over $500</b> goes to a team lead first. This overrides rules 1 and 2.</li>
        </ol>
      </aside>

      <mv-trio-check id="tc-desk" label="Refund tickets">
        <ol class="mv-trio-check-list">
          <li class="mv-trio-check-item" data-trio-item="t4127" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">1</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4127">Unworn trail shoes, delivered 12 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4127-h">T-4127 · Priya Nair · $129.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4127" aria-describedby="tc-t4127-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund" selected>Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4131" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">2</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4131">Jacket with tags on, delivered 45 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4131-h">T-4131 · Lucas Moreau · $210.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4131" aria-describedby="tc-t4131-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit" selected>Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4138" data-trio-state="confirmed">
            <span class="mv-trio-check-num" aria-hidden="true">3</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4138">Tent pole snapped in the box, photo attached</label>
              <span class="mv-trio-check-hint" id="tc-t4138-h">T-4138 · Aiko Tanaka · $340.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4138" aria-describedby="tc-t4138-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace" selected>Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4142">
            <span class="mv-trio-check-num" aria-hidden="true">4</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4142">Two unworn parkas, delivered 9 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4142-h">T-4142 · Mateo García · $1,180.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4142" aria-describedby="tc-t4142-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate" selected>Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4150">
            <span class="mv-trio-check-num" aria-hidden="true">5</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4150">Final-sale swimsuit, never worn</label>
              <span class="mv-trio-check-hint" id="tc-t4150-h">T-4150 · Amara Okafor · $48.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4150" aria-describedby="tc-t4150-h">
                <option value="" disabled>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit" selected>Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4153">
            <span class="mv-trio-check-num" aria-hidden="true">6</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4153">Received size M instead of L, photo attached</label>
              <span class="mv-trio-check-hint" id="tc-t4153-h">T-4153 · Jonas Lindqvist · $95.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4153" aria-describedby="tc-t4153-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4159">
            <span class="mv-trio-check-num" aria-hidden="true">7</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4159">Unworn backpack, delivered 75 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4159-h">T-4159 · Sofia Rossi · $160.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4159" aria-describedby="tc-t4159-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4164">
            <span class="mv-trio-check-num" aria-hidden="true">8</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4164">Unused camp stove set, delivered 40 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4164-h">T-4164 · Daniel Kim · $620.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4164" aria-describedby="tc-t4164-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
          <li class="mv-trio-check-item" data-trio-item="t4170">
            <span class="mv-trio-check-num" aria-hidden="true">9</span>
            <div class="mv-trio-check-text">
              <label class="mv-trio-check-prompt" for="tc-t4170">Gloves that did not fit, delivered 3 days ago</label>
              <span class="mv-trio-check-hint" id="tc-t4170-h">T-4170 · Fatima Zahra · $35.00</span>
            </div>
            <div class="mv-trio-check-control">
              <select class="mv-select" id="tc-t4170" aria-describedby="tc-t4170-h">
                <option value="" disabled selected>Choose an action…</option>
                <option value="refund">Refund to original payment</option>
                <option value="credit">Refund as store credit</option>
                <option value="replace">Send a free replacement</option>
                <option value="escalate">Escalate to a team lead</option>
                <option value="decline">Decline the request</option>
              </select>
            </div>
          </li>
        </ol>
        <button slot="complete" type="button" class="mv-button" data-size="sm">Continue to module 3</button>
      </mv-trio-check>
    </div>
    <div class="tc-foot">
      <p class="tc-log" id="tc-log">Progress restored: 3 tickets confirmed last session.</p>
      <span class="tc-actions">
        <label class="mv-choice">
          <input type="checkbox" role="switch" class="mv-switch" id="tc-auto">
          <span class="mv-choice-text"><span class="mv-choice-title">Verify automatically</span></span>
        </label>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tc-restart">Restart module</button>
      </span>
    </div>
  </section>

  <!-- Language exercise: items rendered from the items property, hashes checked in the browser, 5 s cooldown -->
  <section class="tc-card tc-lang" aria-labelledby="tc-lang-title">
    <div class="tc-head">
      <div>
        <h3 id="tc-lang-title">Spanish A2 · Around the house</h3>
        <p>Type the Spanish word or pick the article. Accents are optional. A wrong set means a 5-second pause.</p>
      </div>
      <span class="mv-badge" data-variant="outline" data-shape="pill">Unit 4 review</span>
    </div>
    <mv-trio-check id="tc-lang" cooldown="5s" salt="es-a2-unit4" label="Spanish vocabulary"></mv-trio-check>
  </section>

  <script type="module">
    await customElements.whenDefined("mv-trio-check");
    const TrioCheck = customElements.get("mv-trio-check");
    const desk = document.getElementById("tc-desk");
    const lang = document.getElementById("tc-lang");
    const log = document.getElementById("tc-log");
    const say = (text) => { log.textContent = text; };

    // Stand-in for your server. It only stores salted hashes (never the answers)
    // and applies the batch rule itself, so the browser learns nothing beyond
    // the ids it confirms.
    const SALT = "nw-refunds-2026";
    const SERVER = {
      t4127: "a3ba7943f2e72ef6b5ca475382d0eca784d445574ede4717e96a36b4a7588c4e",
      t4131: "e7cc294f4a77a7d1c10c4142739cda2431b2b6869a4ef6684dda97e971e86a01",
      t4138: "d922dfd2e3fd4310170c1fd92679c589ebb0499e6f737b1a203acc0240db3dd2",
      t4142: "d2fc233747364ef0232c0f5d1fdba7cdaea97e82d01ecc94bf3444b7331eb0e9",
      t4150: "a6776de3017d2432c3d1bf6b0a9b8afcdac2c5791f720a924be4e046cac1816a",
      t4153: "b0149df55dbf622aeedffad0d4e62f3721d3924321aef4fbc765d5150ae48448",
      t4159: "d4af7ffa16863311616fcddf768dbd75c2338916943075eb4743fe2e6cedf129",
      t4164: "3eb3c92353866407a49c892d0eff0ce98964ead87c5d92366a0acc6d3b03b00d",
      t4170: "fac735b4ff9b86b4883c067b073aebed49ec0e7740641933b722a296fae220e3",
    };
    const fakeServer = async (answers) => {
      await new Promise((r) => setTimeout(r, 700));
      const ids = Object.keys(answers);
      const size = Math.min(3, ids.length);
      const right = [];
      for (const id of ids) {
        if (answers[id] && (await TrioCheck.hash(answers[id], { id, salt: SALT })) === SERVER[id]) right.push(id);
      }
      return right.slice(0, Math.floor(right.length / size) * size);
    };
    desk.check = fakeServer;

    desk.addEventListener("mv-attempt", (e) => say(`Attempt ${e.detail.attempt}: sending ${Object.values(e.detail.answers).filter(Boolean).length} answers…`));
    desk.addEventListener("mv-miss", (e) => say(`Attempt ${e.detail.attempt}: no set of three was fully right.`));
    desk.addEventListener("mv-confirm", (e) => { if (e.detail.source !== "api") say(`Attempt ${e.detail.attempt}: locked ${e.detail.ids.map((id) => id.replace("t", "T-")).join(", ")}.`); });
    desk.addEventListener("mv-complete", (e) => say(`Module complete: ${e.detail.total} tickets in ${e.detail.attempts} attempt${e.detail.attempts === 1 ? "" : "s"}.`));
    document.getElementById("tc-auto").addEventListener("change", (e) => { desk.auto = e.target.checked; });
    document.getElementById("tc-restart").addEventListener("click", () => { desk.reset(); say("Module restarted."); });

    // Rendered items. answerHash = TrioCheck.hash(answer, { id, salt }), computed at authoring time.
    const articles = [{ value: "el", label: "el" }, { value: "la", label: "la" }];
    lang.items = [
      { id: "kitchen", prompt: "the kitchen", hint: "Where you cook", placeholder: "Spanish word", value: "cocina",
        answerHash: ["a4896e33902ad389f472740db007eea22e43d590fc1f4c5c430c9dd6970b7724", "fb069ef1f4fa3413edaabc7a72a7a30c8735520f0f14132edf5f043ceb09fbda"] },
      { id: "window", prompt: "the window", hint: "You open it for fresh air", placeholder: "Spanish word",
        answerHash: ["c31ccf4196fdeaf60478cf4e644eb11fea7de36ed9429f44a837335499f62cb1", "337ffcd8d39756b9286b0007c5ac8dc15c8e73ed6c2964a1083ed50724a76e6e"] },
      { id: "key", prompt: "the key", hint: "It opens the front door", placeholder: "Spanish word",
        answerHash: ["280dbdb70b411e2b86127ec29b7dd70b69ad984b63cfa4342e3c206166bea1e7", "d75dcae5ca5e689048bbec201542f2961c9c46d72d194585d69198881e618649"] },
      { id: "mesa", type: "choice", prompt: "___ mesa", hint: "table", options: articles, value: "la",
        answerHash: "ac606155a337df73bd53b2ddd39beefa03f47acc984a571f81b7f9d38fa56310" },
      { id: "sofa", type: "choice", prompt: "___ sofá", hint: "sofa", options: articles,
        answerHash: "f6c260d5de282870e993113b9723a561b7aeb4250716f78fc1ddd976dd7435f5" },
      { id: "mano", type: "choice", prompt: "___ mano", hint: "hand", options: articles,
        answerHash: "bd9744211255e86a44adea7ad774e131c567e1b15a9c4c33cd6523d7b247c4e7" },
    ];
  </script>
</div>

API

Attributes

Name类型DefaultDescription
batchnumber3How many answers must be right at once to be locked in. Correct answers lock in whole sets only (floor(correct / batch) × batch, in document order); when fewer items than batch are left open, the last set is whatever remains. 1 turns it into plain per-answer verification.
cooldowntime ("5s", "800ms", "1m", ms number)Pause imposed after an attempt that locked nothing: the Verify button reads “Try again in 4s”, drains back and stays aria-disabled. No cooldown by default.
autobooleanVerify on its own 650 ms after an answer is committed (change event: select picked, radio chosen, text field left), whenever enough answers are filled and no cooldown runs. Identical answer sets are never resubmitted.
saltstringSalt mixed into data-answer-hash hashes (see hash()). Ignored when check is set.
normalizefold | trim | exactfoldHow answers are normalized before hashing: fold trims, collapses spaces, lowercases and strips accents (“La Cocína ” = “la cocina”); trim only trims and collapses spaces; exact hashes the raw value. check() always receives raw values.
labelstringKnowledge checkAccessible name of the group (aria-label on the host, unless it has aria-labelledby).
data-trio-itemstring (item id)Marks an item. Its value is the id used in answers, events and hashes; when empty, the element id or a generated id is written back.
data-answer-hashhex SHA-256, space or comma separatedOn an item: accepted answer hashes (several for synonyms such as “cocina” and “la cocina”). Used when no check callback is set.
data-trio-fieldmarker attributeInside an item: the control whose value property is the answer (a custom element, a combobox…). Otherwise the first text field, textarea or select, the checked radio, or the sorted checked checkbox values joined with commas.
data-trio-valuestringOn an item: the answer itself, set by your code for custom widgets (drag and drop, maps, pickers). Takes precedence over any field.
data-trio-statusmarker attributeInside an item: where the confirmed status (seal + “Confirmed”) is written. Otherwise a .mv-trio-check-status span is appended to the item.
data-trio-stateempty | answered | confirmedSet by the component on each item. Server-rendered items with data-trio-state="confirmed" are locked silently on start (restored progress). data-trio-batch holds the set number of a confirmed item.
data-stateidle | ready | pending | cooldown | completeSet on the host: not enough answers yet, verifiable, checking, cooling down, all confirmed. aria-busy is set while checking.

Properties

Name类型Description
check(answers: Record<id, string>) => Promise<string[] | Set<string> | Record<id, boolean>>Your verifier. Receives every open (unconfirmed) item, empty string when unanswered, and resolves to the ids that are correct. For tamper-proof assessments, apply the same batch rule on the server and return only the ids you confirm: the component’s rule is idempotent. A rejection shows “Your answers couldn’t be checked” and spends no lock.
itemsArray<{ id, prompt, hint?, type?: "text" | "select" | "choice", options?: (string | { value, label })[], placeholder?, value?, answerHash?: string | string[] }>Rows rendered by the component into an <ol class="mv-trio-check-list"> before the bar: labelled text field (mv-input), select (mv-select, with a disabled placeholder) or a radio group of choice chips. type defaults to select when options are given, else text.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (keys: label, verify, cooldown ({s}), progress ({confirmed}, {total}), answered ({n}), rule ({batch}), ruleOne, needMore ({n}), miss, missCooldown ({s}), repeat, confirmed ({n}), confirmedOne, rest, ready, error, complete ({total}), completeText ({n}), completeTextOne, announceComplete ({total}, {n}), status, statusSr ({batch}), meter, choose). English defaults.
answersRecord<id, string>Current value of every item, confirmed or not (read-only).
confirmedstring[]Ids of the confirmed items in document order (read-only).
progress{ confirmed, total, answered, attempts, complete }Counts shown in the bar; answered counts open items with a value (read-only).
attempts / pending / coolingnumber / boolean / number (ms)Attempts spent, whether a check is in flight, time left in the cooldown (read-only).

Methods

NameDescription
verify({ reason? })Submits the open answers like the Verify button (reason defaults to "api"). Resolves to the ids locked by this attempt, [] when nothing was locked or the attempt was refused (not enough answers, cooldown, pending, identical to the last failed set, mv-attempt cancelled).
MvTrioCheck.hash(answer, { id, salt, normalize })Static: resolves to the hex SHA-256 of ${salt}:${id}:${normalized answer}, the format data-answer-hash expects. Run it at authoring time or on your server; requires a secure context (HTTPS or localhost).
confirm(ids, { animate? })Locks items without checking, e.g. progress restored from your server. Emits mv-confirm with source "api" (and mv-complete if everything is confirmed). Silent unless animate is true.
reset({ keepAnswers? })Unlocks every item, restores each control’s own readonly / disabled state, clears the answers (unless keepAnswers), the attempts, the cooldown and the feedback.

Events

NameDescription
mv-attemptCancelable, before an attempt is sent. detail: { attempt, answers (open items only), batch (set size for this attempt), reason: "button" | "keyboard" | "auto" | "api" }. preventDefault() refuses it without spending an attempt (e.g. a daily limit).
mv-confirmItems were locked. detail: { ids, items (elements), attempt, source (the attempt reason, or "api"), confirmed, total }.
mv-missAn attempt locked nothing. detail: { attempt, answered, cooldown (ms, 0 when none) }. Never says which answers were wrong.
mv-completeEvery item is confirmed. detail: { attempts, total, duration (ms since the first attempt) }.
mv-check-errorThe check callback rejected, or hashing failed (insecure context). detail: { attempt, error }.

Content structure

NameDescription
(content)Your items: any structure with [data-trio-item] elements (use .mv-trio-check-list / .mv-trio-check-item for the default row layout), or nothing when you use the items property. The bar is appended last.
completeChildren with slot="complete" (a Continue button, a certificate link) are moved into the bar and shown only once everything is confirmed.

CSS classes

NameDescription
mv-trio-check-list / mv-trio-check-itemDefault list and row layout (number, text, control, status columns; the control goes under the prompt below 34rem of container width). Parts: -num, -text, -prompt, -hint, -control.
mv-trio-check-choices / mv-trio-check-choiceChoice chips: a group of labels wrapping native radios (arrow keys, focus ring, checked state).
mv-trio-check-status / mv-trio-check-sealConfirmed status of an item: the stamped seal (-seal-check path) and -status-text. The row gets data-trio-fresh for the stamp animation, staggered with --_i.
mv-trio-check-barSticky summary bar: -done-seal, -summary (-head with -count and -answered, -meter of -set groups of -tick[data-tick="empty | answered | confirmed"], -message[data-tone="miss | repeat | success | error | complete"]), -actions (-verify button with data-cooling, -extra for the complete slot).

CSS variables

NameDefaultDescription
--mv-trio-check-sealvar(--mv-success)Seal, confirmed rows and confirmed ticks.
--mv-trio-check-gapvar(--mv-space-2)Space between rows of the default list.
--mv-trio-check-sticky-offsetvar(--mv-space-3)Distance of the sticky bar from the bottom of the viewport while a long list scrolls.

Accessibility

The host is a labelled group; every rendered field has a real <label> (choice chips are a labelled radiogroup of native radios, arrow keys included) and hints are linked with aria-describedby. Nothing ever signals whether an individual answer is right, to sighted users or to screen readers alike. When a set is locked, text fields become readonly (still focusable and readable), selects and radios become disabled, and each control is described by the item’s status (“Confirmed and locked, set 2”); the visible “Confirmed” text and the check mark in the seal mean it never relies on color. If focus sat on a control that just became disabled (auto mode), it moves to the next open field, or to the Verify button. A polite live region announces each outcome once: “3 answers confirmed. 6 of 12 confirmed.”, “Not enough correct yet. Nothing was locked. You can try again in 5 seconds.”, “You can verify again.” when a cooldown ends, and “All 12 answers confirmed. Finished in 4 attempts.”; the visible message line repeats it and is linked to the Verify button with aria-describedby. The Verify button stays focusable when it cannot be used (aria-disabled, with the reason in the message: “Answer 2 more to verify.”), shows aria-busy while checking, and Ctrl/Cmd+Enter anywhere in the component verifies (aria-keyshortcuts). The meter is a role="progressbar" with “6 of 12 confirmed” as its value text. Reduced motion (OS or data-motion="reduce"): no stamp, ink ring, shake or draining fill, states change instantly and the countdown text remains. Forced colors: ticks and seals use system colors, confirmed rows get a solid outline. Locked selects and radios are disabled, so they are not submitted with a native form: read answers or confirmed instead. Known limits: data-answer-hash keeps answers out of plain sight, not secret; a small answer space (a select, el / la) can be tried offline, so use check() on your server when the result matters.

本页面由 AI 翻译。报告翻译问题