익스클루시브

Trust Ladder <mv-trust-ladder>

실적으로 얻는 단계적 자율성으로, AI 에이전트, 자동화, 연동, 새 팀원 모두에 적용됩니다.

문화적 레퍼런스

어린 왕자, 앙투안 드 생텍쥐페리 (1943, 도서). 여우는 길들이려면 인내가 필요하다고 말합니다. 매일 같은 시간에 조금씩 더 가까이 앉아야 하며, 신뢰는 한 번에 쌓이지 않고 한 걸음씩 쌓입니다. UI에서는 각 기능이 실적이 쌓일 때만(연속 승인 횟수, 일수) 자율성의 사다리를 올라가고, 각 단계는 사람이 부여하며, 약속을 어기면 한 단계 내려가 횟수를 처음부터 다시 셉니다.

작동 방식

실적으로 얻는 단계적 자율성으로, AI 에이전트, 자동화, 통합, 새 팀원 모두에 적용됩니다. 각 기능(“Refund customers”, “Deploy to staging”, “Approve expenses”)은 신뢰 매트릭스의 한 행이며, 올라가는 가로대가 자체 사다리에서의 수준을 보여 주고(Suggest only → Act with approval → Act and report → Act silently under $50), 각 수준은 도달 조건을 명시합니다. 연속 승인 횟수, 최소 작업 수, 승인율, 현재 수준에서의 일수, 허용되는 되돌림 작업 수 등입니다. 행을 열면 현재 단계가 표시된 계단 형태의 사다리, 값과 막대가 있는 다음 수준 기준 체크리스트(“14 of 20”, “9 of 14 days”)와 남은 조건을 평이한 문장으로 설명한 내용(“Still needed: 6 more approved in a row and 5 more days”), 끊기지 않은 끝부분이 연속 기록을 나타내는 실적 띠, 집계, 그리고 결과(approved, kept, edited, rejected, reverted)와 검토자가 표시된 최근 작업 표가 나타납니다. record()로 결과를 입력하며, 되돌린 작업이 생기면 즉시 한 단계 강등을 제안하거나(“Step down to Act with approval until the record is clean again?”) auto-demote를 사용하면 직접 강등합니다. “Keep level, restart count”를 선택하면 대신 집계 기간을 초기화합니다. 승격은 require-owner를 사용하면 언제나 사람이 결정하며(기능의 소유자에게만 제안되고 자동으로 이루어지지 않음), 실적이 채워지기 전에 “Promote now”를 누르면 확인을 요청하고 부족한 항목을 나열합니다. mv-promote와 mv-demote는 취소할 수 있고 서버용 waitUntil(promise)를 받습니다. 컴포넌트는 권고하고, 앱이 집행합니다. 대시보드용 컴팩트 보기를 제공하며, 모든 텍스트를 재정의할 수 있습니다.

카테고리데이터 표시
유형Web Component (<mv-trust-ladder>)
상태안정
키트검증할 수 있는 AI
함께 설치되는 항목button
Keywordsexclusive, culture, autonomy, trust, ai-agent, agent, automation, human-in-the-loop, approval, permissions, graduated, track-record, promotion, demotion, governance, onboarding, matrix, ladder

When to use

  • An AI agent or automation should earn more autonomy per task from its approved, edited and reverted actions
  • A settings page must show what an assistant may do on its own today and what it takes to unlock the next level
  • New team members or junior approvers get wider rights step by step, based on a reviewed track record
  • A reverted action by a bot or integration should immediately prompt a human to reduce its autonomy

Avoid when

  • Each critical action needs a second person to co-sign it, whatever the track record → use Two-Key instead
  • A few one-off exceptions to a rule are budgeted per period rather than earned over time → use Pass Card instead
  • Rights are static roles or permissions set by an admin, with no track record behind them: a plain settings table is clearer

설치

node scripts/add.mjs trust-ladder --out ./src/marvelous

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

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

사용법

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

<mv-trust-ladder label="Support assistant">
  <script type="application/json">{ "capabilities": [{ "id": "refunds", "label": "Refund customers", "level": "approve", "since": "-12d",
    "evidence": [{ "at": "-2h", "title": "Refund $42 · Order 4821", "outcome": "approved" }, { "at": "-1d", "title": "Refund $18 · Order 4790", "outcome": "reverted" }] }] }</script>
</mv-trust-ladder>

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

<div id="tl-demo" style="width:min(100%,62rem);margin-inline:auto">
  <style>
    #tl-demo { display:grid; gap:1.25rem; align-content:start }
    #tl-demo .tl-top { display:flex; align-items:flex-end; justify-content:space-between; gap:.5rem 1rem; flex-wrap:wrap }
    #tl-demo .tl-kicker { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #tl-demo .tl-top h3 { margin:.125rem 0 0; font-size:1.125rem; letter-spacing:-.01em }
    #tl-demo .tl-top p.tl-sub { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #tl-demo .tl-grid { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:1.25rem; align-items:start }
    #tl-demo .tl-panel { display:grid; gap:.875rem; padding:1rem 1.125rem 1.125rem; border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-xl) }
    #tl-demo .tl-panel h4 { margin:0; font-size:.8125rem; font-weight:600 }
    #tl-demo .tl-row { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #tl-demo .tl-row > span { color:var(--mv-fg-muted); font-size:.75rem; margin-inline-end:.25rem }
    #tl-demo .tl-panel .mv-choice { font-size:.8125rem }
    #tl-demo .tl-log { margin:0; min-height:2.5em; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono) }
    #tl-demo .tl-side { display:grid; gap:.5rem }
    #tl-demo .tl-side p { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
    @media (max-width:48rem) { #tl-demo .tl-grid { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="tl-top">
    <div>
      <p class="tl-kicker">Helpdesk · Automation settings</p>
      <h3>What the support assistant may do on its own</h3>
      <p class="tl-sub">Autonomy is earned per capability from its track record. Your app applies the level.</p>
    </div>
  </div>

  <!-- Main: an AI support assistant, four capabilities, refunds expanded -->
  <mv-trust-ladder id="tl-main" label="Support assistant" user="Priya Nair" require-owner evidence-limit="5"></mv-trust-ladder>

  <div class="tl-grid">
    <section class="tl-panel" aria-label="Demo controls">
      <h4>Try it on “Refund customers”</h4>
      <div class="tl-row">
        <span>Next refund:</span>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-approve">Approved</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-edit">Edited</button>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tl-revert">Reverted</button>
      </div>
      <div class="tl-row">
        <button type="button" class="mv-button" data-variant="secondary" data-size="sm" id="tl-week">Skip ahead a week</button>
        <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="tl-reset">Reset demo</button>
      </div>
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="tl-owner" checked> Viewing as the owner, Priya Nair</label>
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="tl-fail"> Server rejects the next change</label>
      <p class="tl-log" id="tl-log" aria-live="polite">Add a few approved refunds, then promote.</p>
    </section>

    <!-- Compact: a new team member's approval rights, same mechanic -->
    <div class="tl-side">
      <mv-trust-ladder id="tl-compact" compact label="Elena Petrova · Junior approver" user="Tomás Rivera" require-owner>
        <script type="application/json">{
          "levels": [
            { "id": "shadow", "label": "Shadow review", "description": "Reviews alongside a senior approver. Decides nothing." },
            { "id": "co", "label": "Co-approve", "description": "Approves together with a second approver.", "criteria": { "actions": 10, "maxReverted": 0 } },
            { "id": "solo", "label": "Approve up to $1,000", "description": "Approves alone up to $1,000.", "criteria": { "streak": 15, "days": 30, "maxReverted": 0 } },
            { "id": "senior", "label": "Approve up to $10,000", "description": "Approves alone up to $10,000.", "criteria": { "streak": 40, "days": 90, "approvalRate": 0.97, "maxReverted": 0 } }
          ],
          "capabilities": [
            { "id": "expenses", "label": "Expense reports", "owner": "Tomás Rivera", "level": "co", "since": "-41d",
              "evidence": [
                { "at": "-4h", "title": "Expense report · Lisbon offsite · $812.40", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-1d", "title": "Expense report · Client dinner · $236.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-3d", "title": "Expense report · Conference pass · $699.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-5d", "title": "Expense report · Taxi receipts · $84.10", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-7d", "title": "Expense report · Laptop stand · $59.99", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-9d", "title": "Expense report · Team lunch · $148.25", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-12d", "title": "Expense report · Train to Lyon · $118.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-14d", "title": "Expense report · Coworking day pass · $35.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-17d", "title": "Expense report · Hotel, Berlin · $412.80", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-19d", "title": "Expense report · Software license · $240.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-22d", "title": "Expense report · Parking · $27.50", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-25d", "title": "Expense report · Flight to Toronto · $684.20", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-28d", "title": "Expense report · Printer ink · $46.99", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-31d", "title": "Expense report · Client gift · $75.00", "outcome": "approved", "by": "Tomás Rivera" },
                { "at": "-34d", "title": "Expense report · Mileage · $92.40", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-38d", "title": "Expense report · Headset · $129.00", "outcome": "approved", "by": "Tomás Rivera" }
              ] },
            { "id": "invoices", "label": "Vendor invoices", "owner": "Tomás Rivera", "level": "co", "since": "-12d",
              "evidence": [
                { "at": "-1d", "title": "Invoice INV-7731 · Cloud hosting · $940.00", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-3d", "title": "Invoice INV-7702 · Cleaning services · $610.00", "outcome": "edited", "by": "Tomás Rivera", "note": "Wrong cost center" },
                { "at": "-6d", "title": "Invoice INV-7688 · Office supplies · $182.35", "outcome": "approved", "by": "Grace Liu" },
                { "at": "-9d", "title": "Invoice INV-7654 · Translation · $420.00", "outcome": "approved", "by": "Tomás Rivera" }
              ] },
            { "id": "travel", "label": "Travel requests", "owner": "Tomás Rivera", "level": "shadow", "since": "-6d",
              "evidence": [
                { "at": "-2d", "title": "Travel · Madrid, 3 nights", "outcome": "approved", "by": "Grace Liu" }
              ] }
          ]
        }</script>
      </mv-trust-ladder>
      <p>Compact view: the same ladder for a new team member’s approval rights. Open a row for details.</p>
    </div>
  </div>

  <script type="module">
    const el = document.getElementById("tl-main");
    const $ = (id) => document.getElementById(id);
    const log = $("tl-log");
    const H = 36e5, D = 864e5;
    const reviewers = ["Daniel Kim", "Sofia Rossi", "Kenji Watanabe", "Lucas Moreau"];
    const reasons = ["damaged in transit", "late delivery", "wrong size", "duplicate charge", "missing item", "not as described"];
    let order = 48213;
    const amount = (i) => ((i * 37) % 180 + 12 + ((i * 13) % 100) / 100).toFixed(2);
    const refund = (i, at, outcome, extra = {}) => ({
      at, outcome, by: reviewers[i % reviewers.length],
      title: `Refund $${amount(i)} · Order #${order - i * 7} · ${reasons[i % reasons.length]}`, ...extra,
    });
    const kept = (label, i, at, extra = {}) => ({ at, outcome: "kept", title: label(i), ...extra });

    const replyLevels = [
      { id: "suggest", label: "Suggest only", description: "Drafts a reply for an agent to send." },
      { id: "approve", label: "Act with approval", description: "Sends once an agent approves each reply.", criteria: { streak: 10, maxReverted: 0 } },
      { id: "report", label: "Act and report", description: "Sends on its own and logs every reply for review.", criteria: { streak: 20, days: 14, maxReverted: 0 } },
      { id: "silent", label: "Act silently", description: "Sends on its own. Weekly spot checks.", criteria: { streak: 50, days: 30, maxReverted: 0 } },
    ];

    const initial = () => {
      const now = Date.now();
      // Refunds: 14 approved in a row since an edit, 9 days at "Act with approval".
      const refunds = [];
      for (let i = 0; i < 14; i++) refunds.push(refund(i, now - (3 + i * 14) * H, "approved"));
      refunds.push(refund(14, now - 8 * D, "edited", { note: "Amount lowered to $32.00: shipping isn’t refundable" }));
      refunds.push(refund(15, now - 8.6 * D, "approved"));
      for (let i = 16; i < 26; i++) refunds.push(refund(i, now - (10 + (i - 16)) * D, "approved"));
      const replies = [];
      const ticket = (i) => `Reply to ticket #${91540 - i * 3} · ${["order status", "return label", "password reset", "invoice copy", "delivery window"][i % 5]}`;
      for (let i = 0; i < 52; i++) replies.push(kept(ticket, i, now - (2 + i * 13) * H));
      const routes = [];
      const route = (i) => `Tag and route ticket #${91602 - i * 2} → ${["Billing", "Shipping", "Returns", "Accounts"][i % 4]}`;
      for (let i = 0; i < 24; i++) routes.push(kept(route, i, now - (1 + i * 6) * H));
      const credits = [];
      const credit = (i) => `Store credit $${(15 + (i * 11) % 60).toFixed(2)} · Account #${20931 + i * 17}`;
      credits.push({ at: now - 20 * H, outcome: "reverted", title: "Store credit $120.00 · Account #20931", by: "Lucas Moreau", note: "Duplicate: the customer was already credited by phone" });
      for (let i = 1; i < 10; i++) credits.push(kept(credit, i, now - (1 + i) * D));
      return [
        {
          id: "refunds", label: "Refund customers", owner: "Priya Nair", level: "approve", since: now - 9 * D, open: true, evidence: refunds,
          levels: [
            { id: "suggest", label: "Suggest only", description: "Drafts the refund. An agent issues it." },
            { id: "approve", label: "Act with approval", description: "Issues a refund once an agent approves it.", criteria: { streak: 10, maxReverted: 0 } },
            { id: "report", label: "Act and report", description: "Issues refunds under $200 and posts each one for review.", criteria: { streak: 20, days: 14, maxReverted: 0 } },
            { id: "silent", label: "Act silently under $50", description: "Issues small refunds without a report. Weekly spot checks.", criteria: { streak: 50, days: 30, approvalRate: 0.98, maxReverted: 0 } },
          ],
        },
        { id: "replies", label: "Reply to customers", owner: "Priya Nair", level: "report", since: now - 32 * D, levels: replyLevels, evidence: replies },
        { id: "routing", label: "Tag and route tickets", owner: "Amara Okafor", level: "silent", since: now - 61 * D, levels: replyLevels, evidence: routes },
        { id: "credit", label: "Issue store credit", owner: "Priya Nair", level: "report", since: now - 12 * D, levels: replyLevels, evidence: credits },
      ];
    };

    let n = 30;
    const reset = () => { n = 30; el.capabilities = initial(); log.textContent = "Add a few approved refunds, then promote."; };
    await customElements.whenDefined("mv-trust-ladder");
    reset();

    $("tl-approve").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "approved")));
    $("tl-edit").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "edited", { note: "Agent changed the amount before approving" })));
    $("tl-revert").addEventListener("click", () => el.record("refunds", refund(n++, Date.now(), "reverted", { note: "The card issuer had already refunded the customer" })));
    $("tl-week").addEventListener("click", () => {
      const caps = el.capabilities;
      for (const c of caps) {
        if (c.since) c.since -= 7 * D;
        if (c.resetAt) c.resetAt -= 7 * D;
        for (const e of c.evidence) e.at -= 7 * D;
      }
      el.capabilities = caps;
      log.textContent = "A week went by: every “days at this level” count moved forward.";
    });
    $("tl-reset").addEventListener("click", reset);
    $("tl-owner").addEventListener("change", (e) => { el.user = e.target.checked ? "Priya Nair" : "Daniel Kim"; });

    const save = (e, verb) => {
      const fail = $("tl-fail").checked;
      // Your server applies the new level here; a rejection changes nothing.
      e.detail.waitUntil(new Promise((ok, ko) => setTimeout(() => (fail ? ko(new Error("403")) : ok()), 650)));
      log.textContent = `mv-${verb} · ${e.detail.label}: ${e.detail.fromLabel} → ${e.detail.toLabel} · reason: ${e.detail.reason}${e.detail.early ? " · early" : ""}${fail ? " · server rejects…" : ""}`;
      if (fail) $("tl-fail").checked = false;
    };
    el.addEventListener("mv-promote", (e) => save(e, "promote"));
    el.addEventListener("mv-demote", (e) => save(e, "demote"));
    el.addEventListener("mv-demote-dismiss", (e) => { log.textContent = `mv-demote-dismiss · ${e.detail.capability}: level kept, count restarted`; });
    el.addEventListener("mv-trust-evidence", (e) => {
      const { entry, status, progress } = e.detail;
      const streak = progress.criteria.find((c) => c.key === "streak");
      log.textContent = `mv-trust-evidence · ${entry.outcome} · status: ${status}${streak ? ` · streak ${streak.value}/${streak.target}` : ""}`;
    });
    el.addEventListener("mv-trust-error", () => { log.textContent = "mv-trust-error · the server refused: nothing changed."; });
  </script>
</div>

API

Attributes

Name유형DefaultDescription
labelstringAutonomyHeading of the matrix (who or what is being trusted: “Support assistant”, “Deploy bot”, “Elena Petrova · Junior approver”). Also names the group for assistive tech.
userstringName of the person viewing. With require-owner, promotions are only offered when it matches the capability’s owner; everyone can still step a capability down.
require-ownerbooleanPromotion is always a person’s decision: auto-promote is ignored, promote({ reason: "criteria" }) is refused, and the Promote button is disabled (with an explanation) for anyone but the owner.
auto-promotebooleanWhen a record() completes the next level’s criteria, promote automatically (mv-promote with reason "criteria", still cancelable). Ignored with require-owner.
auto-demotebooleanA reverted action steps the capability down right away (mv-demote with reason "reverted", cancelable) instead of only suggesting it.
compactbooleanDense matrix for dashboards and side panels: name, rungs and status only; rows still open for details.
evidence-limitnumber5Recent actions shown in the table before “Show all”.
heading-levelnumber (2-5)3Heading level of each capability row (section titles inside use the next level).
localestringen-USLocale for dates, times and lists.

Properties

Name유형Description
capabilitiesArray<{ id, label, owner?, level, since?, resetAt?, levels?, evidence[], open? }>The rows. level = id (or index) of the current level; since = when it was reached; evidence = [{ id?, at, title, outcome: "approved" | "kept" | "edited" | "rejected" | "reverted", by?, note? }]. Times accept Date, epoch ms, ISO strings or relative offsets ("-3d", "-5h", "now"). Reading it returns the live state, including status. Also accepted as a <script type="application/json"> child: { levels, capabilities }.
levelsArray<{ id, label, description?, criteria? }>Default ladder for capabilities without their own levels, lowest first. criteria (to reach that level, counted since the current level was reached or the count restarted): streak (approved or kept in a row), actions, approvalRate (0-1 or %), days, maxReverted. Defaults: Suggest only → Act with approval → Act and report → Act silently.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (English defaults), e.g. statusReady, promote ({level}), stepDown, alertDemote ({action}, {when}, {level}), keep, ownerOnly ({owner}), critStreak, needStreak ({n}).

Methods

NameDescription
record(id, { title, outcome, at?, by?, note? })Adds an outcome to a capability’s record, re-evaluates it and emits mv-trust-evidence. A reverted action raises the step-down suggestion (or demotes with auto-demote). Returns the stored entry, or null.
promote(id, { reason? })One level up. Emits the cancelable mv-promote; resolves true once applied (after any waitUntil promises). Early promotions are allowed from code; detail.early and detail.unmet say what was missing.
demote(id, { to?, reason?, trigger? })One level down (or to level id/index to). Emits the cancelable mv-demote; resolves true once applied.
keep(id)Keeps the level after a reverted action and restarts the count (the criteria window starts now). Emits the cancelable mv-demote-dismiss.
progress(id){ level, next, status, met, ratio, criteria: [{ key, label, value, target, met, ratio, text }], stats: { streak, actions, clean, corrected, reverted, days, rate, since } }.
expand(id) / collapse(id) / toggle(id)Opens or closes a row’s details.

Events

NameDescription
mv-promoteCancelable, before a promotion. detail: { capability, label, from, to, fromLabel, toLabel, reason: "owner" | "criteria" | "api", early, unmet[], stats, user, waitUntil(promise) }. A rejected promise changes nothing and announces an error.
mv-demoteCancelable, before a step down. detail: { capability, label, from, to, fromLabel, toLabel, reason: "owner" | "reverted" | "api", trigger (the reverted entry, if any), user, waitUntil(promise) }.
mv-trust-evidenceAn outcome was recorded. detail: { capability, entry, status: "building" | "ready" | "demote" | "reverted" | "top", suggestion: "promote" | "demote" | null, progress }.
mv-demote-dismissCancelable: someone chose “Keep level, restart count” after a reverted action. detail: { capability, level, trigger, user }.
mv-trust-errorA waitUntil promise rejected. detail: { capability, action: "promote" | "demote", error }.

Content structure

NameDescription
(json)An optional <script type="application/json"> child with { levels, capabilities } for declarative setup (ignored when the properties are set first).

CSS classes

NameDescription
mv-trust-ladder-rowOne capability (li): data-status="building | ready | demote | reverted | top", data-open, data-busy. Holds -trigger (disclosure button with -cap, -track of -rung[data-state], -level, -status, -chevron) and -detail.
mv-trust-ladder-steps / -stepThe ladder as an ordered list drawn as a staircase; each step has data-state="reached | current | next | locked" (aria-current="step" on the current one) and data-ready on a next step whose criteria are met.
mv-trust-ladder-criteria / -criterionNext level’s criteria: data-met, -criterion-icon, -label, -value, -bar (--_p).
mv-trust-ladder-strip / -cellDecorative track-record strip (aria-hidden): one cell per outcome, data-outcome, data-streak, data-before.
mv-trust-ladder-tableRecent actions table (When, Action, Outcome, Reviewed by); rows carry data-outcome and data-before.
mv-trust-ladder-alert / -actions / -confirmStep-down suggestion, action bar (note + Step down / Promote) and early-promotion confirmation.

CSS variables

NameDefaultDescription
--mv-trust-ladder-accentvar(--mv-accent)Reached rungs and steps, streak cells, progress bars.
--mv-trust-ladder-readyvar(--mv-success)Met criteria and “Ready to promote”.
--mv-trust-ladder-riskvar(--mv-danger)Reverted actions and the step-down suggestion.
--mv-trust-ladder-correctedvar(--mv-warning)Edited and rejected outcomes, early-promotion confirmation.
--mv-trust-ladder-rise0.625remHeight of each stair in the ladder.

Accessibility

The element is a group labelled by its heading; each capability is a list item whose heading contains a real disclosure <button> (aria-expanded, aria-controls) that reads the whole row in words: name, owner, current level with “Level 2 of 4”, and the status (“Ready to promote”, “Step down suggested”, “64% to next level”). The rungs and the track-record strip are aria-hidden; the same facts are always in text. The opened region is labelled by its trigger. Levels are an ordered list with the current step marked by aria-current="step" and a visible “Current level” label; reached, next (with its percentage) and locked steps are named in text too. Criteria are a list where each item says its value (“14 of 20”) and, for screen readers, “met” or “not met yet”, followed by a sentence of what is still needed. Evidence is a real table with a caption and column headers; outcomes combine an icon and a word, and rows from before the current count say so. Status never relies on color: icons, words and dashed or solid borders carry it. Promote and Step down are buttons whose names include the target level; a Promote the viewer may not use is aria-disabled and described by the “Only Priya Nair can promote” note. Early promotion opens an inline confirmation (focus moves to Cancel, Escape closes it and returns focus to Promote; confirming also puts focus back on the Promote control). While a change awaits the server the button is aria-busy and the row says “Saving…”. Changes are announced politely (“Refund customers promoted to Act and report. The count starts again at this level.”, “…is ready to promote”), a reverted action assertively; errors assertively. If a change hides the focused button, focus moves to the next sensible control in the row. Reduced motion (OS or data-motion="reduce"): the step highlight, row reveal and new-row fade are instant. Forced colors: rungs and cells get system-color borders and fills, the current step a Highlight outline and progress bars a Highlight fill. Known limits: the component only displays and requests levels; the app enforces them (it must refuse on the server an action the current level does not allow), and the criteria are computed from the evidence you pass, not from actions it observes.

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