Exclusivobeta

Pass Card <mv-pass-card>

Um pequeno orçamento visível de exceções a regras (“passes”) para os momentos em que uma regra precisa ceder: fazer merge durante um congelamento de release, pular uma verificação obrigatória, uma entrega atrasada, uma compra acima do limite de gastos, uma remarcação sem taxa, uma exceção de SLA. A carteira mostra “2 of 3 left this quarter · Refills Oct 1” com um bilhete picotado por passe: os disponíveis são sólidos, um passe em vigor leva um raio, um aguardando aprovação é listrado, um usado é vazado e perfurado, e o bilhete vira visivelmente quando um passe é usado. Onde uma regra bloqueia a ação (blocked, rule, action), o portão oferece “Use a pass”: um formulário na própria página que permite ao usuário escolher um tipo de passe (cada um com sua duração, seu limite por período e sua regra de aprovação), mostra em duas colunas exatamente o que o passe cobre e o que não cobre, exige um motivo identificado com tamanho mínimo, pede um aprovador quando o passe precisa de um (ele fica então em espera, não em vigor) e mostra quantos restarão depois do uso antes de gastar qualquer coisa. Usar um passe passa por um mv-pass-use cancelável cujo waitUntil(promise) permite que seu servidor o registre (uma rejeição não gasta nada); o portão então vira “Pass in effect · covers PR #4821 until 3:40 PM” e o passe expira no horário. Cada uso entra em uma tabela de registro de passes (quando, quem, passe e alvo, motivo, status: in effect, awaiting approval, used, returned, declined) que os revisores podem ler; passes não usados podem ser devolvidos (mv-pass-return) e não contam, as aprovações são resolvidas com approve() / deny(), e “Request more” emite mv-pass-request. Configuração declarativa por um filho JSON, com todos os textos em inglês substituíveis.

CategoriaFormulários
TipoWeb Component (<mv-pass-card>)
Statusbeta
KitAções destrutivas e segredos com segurança
Também instalabutton, field, radio, checkbox, select, textarea
Keywordsexclusive, culture, exception, override, waiver, exemption, late-pass, extension, merge-freeze, bypass, allowance, quota, budget, audit-log, accountability, approval, policy, compliance, ticket

When to use

  • Engineers may occasionally bypass a merge freeze or a required check, but each bypass must be justified and reviewable
  • Students or customers get a few extensions, late passes or free reschedules per term, and should see how many they have left
  • A spending limit, approval step or SLA rule can be exceeded a limited number of times, sometimes with a manager's sign-off
  • Admins want exceptions to stay rare and accountable without making every one a support ticket

Avoid when

  • Every critical action needs a second person to co-sign it, not a limited budget of exceptions → use Two-Key instead
  • The limit is a metered quota or rate that refills continuously, like API calls or AI messages → use Recharge instead
  • Elevated rights are granted for a time window and the UI must revert when it ends, with no per-period budget → use Midnight instead

Instalação

node scripts/add.mjs pass-card --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["pass-card"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/pass-card/pass-card.js, components/pass-card/pass-card.css, components/button/button.css, components/field/field.css, components/radio/radio.css, components/checkbox/checkbox.css, components/select/select.css, components/textarea/textarea.css, components/textarea/char-count.js.

Uso

Início rápido, a menor marcação que funciona:

<mv-pass-card label="Freeze passes" total="3" blocked rule="Merging to main is frozen until Oct 3" action="PR #4821"></mv-pass-card>

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="pc-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #pc-demo { display:grid; gap:1.25rem; align-content:start }
    #pc-demo .pc-pr { display:grid; gap:0; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle); overflow:hidden }
    #pc-demo .pc-pr-head { display:grid; gap:.375rem; padding:1rem 1.25rem; background:var(--mv-surface); border-bottom:1px solid var(--mv-border) }
    #pc-demo .pc-pr-title { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; margin:0; font-size:1.0625rem; font-weight:650; letter-spacing:-.01em }
    #pc-demo .pc-pr-title span { color:var(--mv-fg-subtle); font-weight:400 }
    #pc-demo .pc-pr-sub { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-pr-sub code { padding:.0625rem .375rem; border-radius:var(--mv-radius-sm); background:var(--mv-bg-muted); font:.75rem var(--mv-font-mono); color:var(--mv-fg) }
    #pc-demo .pc-checks { display:grid; gap:0; margin:0; padding:0; list-style:none; background:var(--mv-surface); border-bottom:1px solid var(--mv-border) }
    #pc-demo .pc-checks li { display:flex; align-items:center; gap:.625rem; padding:.625rem 1.25rem; font-size:.8125rem; border-top:1px solid var(--mv-border) }
    #pc-demo .pc-checks li:first-child { border-top:0 }
    #pc-demo .pc-checks svg { width:1rem; height:1rem; flex:none }
    #pc-demo .pc-ok { color:color-mix(in oklab, var(--mv-success), var(--mv-fg) 25%) }
    #pc-demo .pc-no { color:color-mix(in oklab, var(--mv-danger), var(--mv-fg) 20%) }
    #pc-demo .pc-checks .mv-badge { margin-inline-start:auto }
    #pc-demo .pc-body { display:grid; gap:1rem; padding:1rem 1.25rem 1.25rem }
    #pc-demo .pc-merge { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap }
    #pc-demo .pc-merge p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-controls { display:flex; align-items:center; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.875rem 1rem; border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-lg) }
    #pc-demo .pc-controls .mv-choice { font-size:.8125rem }
    #pc-demo .pc-log { flex:1 1 14rem; min-width:0; margin:0; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); text-align:end }
    #pc-demo .pc-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,24rem),1fr)); gap:1.25rem; align-items:start }
    #pc-demo .pc-panel { display:grid; gap:.875rem; padding:1rem 1.125rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #pc-demo .pc-kicker { margin:0; color:var(--mv-fg-subtle); font-size:.6875rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase }
    #pc-demo .pc-assign { display:grid; gap:.25rem }
    #pc-demo .pc-assign strong { font-size:.9375rem; letter-spacing:-.01em }
    #pc-demo .pc-assign span { color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-expense { display:flex; align-items:baseline; justify-content:space-between; gap:.75rem; flex-wrap:wrap }
    #pc-demo .pc-expense strong { font-size:.9375rem }
    #pc-demo .pc-expense span { font-variant-numeric:tabular-nums; font-weight:600 }
    #pc-demo .pc-note { margin:0; color:var(--mv-fg-muted); font-size:.75rem }
  </style>

  <!-- Main: a pull request blocked by a release freeze, with a team budget of freeze passes -->
  <section class="pc-pr" aria-label="Pull request #4821">
    <header class="pc-pr-head">
      <p class="pc-pr-title">Fix rounding in checkout totals <span>#4821</span></p>
      <p class="pc-pr-sub">Maya Chen wants to merge 3 commits into <code>main</code> from <code>fix/checkout-rounding</code></p>
    </header>
    <ul class="pc-checks">
      <li><svg class="pc-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>All 14 checks passed</li>
      <li><svg class="pc-ok" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>Approved by Jonas Weber and Leila Haddad</li>
      <li id="pc-freeze-row"><svg class="pc-no" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>Release freeze for 4.2 (until Oct 3)<span class="mv-badge" data-variant="danger" id="pc-freeze-badge">Blocking</span></li>
    </ul>
    <div class="pc-body">
      <mv-pass-card id="pc-main" label="Freeze passes" total="3" period="this quarter" refills="2026-10-01"
        blocked rule="Merging to main is frozen until Oct 3 for the 4.2 release"
        action="PR #4821 · Fix rounding in checkout totals" user="Maya Chen" requestable log-limit="4">
        <script type="application/json">{
          "approvers": ["Priya Nair · Release captain", "Tomás Rivera · Engineering manager"],
          "kinds": [
            { "id": "freeze", "label": "Merge during freeze", "duration": "24h",
              "covers": ["Merging this pull request to main", "The automatic deploy to staging that follows"],
              "excludes": ["Production deploys (the release captain ships those)", "Skipping required reviews or failing checks"] },
            { "id": "hotfix", "label": "Production hotfix", "description": "Ship to production now", "duration": "4h", "limit": 1, "approval": true,
              "covers": ["Merging and deploying this pull request to production", "One rollback if the deploy misbehaves"],
              "excludes": ["Database migrations", "Any other pull request"] }
          ]
        }</script>
      </mv-pass-card>
      <div class="pc-merge">
        <p id="pc-merge-note">Merging is blocked by the release freeze.</p>
        <button type="button" class="mv-button" id="pc-merge" aria-disabled="true" aria-describedby="pc-merge-note">Merge pull request</button>
      </div>
    </div>
  </section>

  <div class="pc-controls">
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="pc-fail"> Server rejects the next pass</label>
    <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="pc-approve" aria-disabled="true">Approve as Priya Nair</button>
    <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="pc-reset">Reset demo</button>
    <p class="pc-log" id="pc-log" aria-live="polite">Pick “Use a pass” to try it.</p>
  </div>

  <div class="pc-grid">
    <!-- Inline: a student's late passes, right on the closed assignment -->
    <section class="pc-panel" aria-label="Assignment">
      <p class="pc-kicker">Course portal · Writing 210</p>
      <div class="pc-assign">
        <strong>Essay 3 · The ethics of automation</strong>
        <span>Due Sep 22, 11:59 PM · 1,500 words · 15% of the grade</span>
      </div>
      <mv-pass-card id="pc-late" variant="inline" label="Late passes" total="4" period="this semester" refills="2027-01-19"
        blocked rule="Submissions closed on Sep 22 at 11:59 PM" action="Essay 3 · The ethics of automation" user="Samuel Adeyemi" min-reason="10">
        <script type="application/json">{
          "kinds": [
            { "id": "late", "label": "48-hour extension", "duration": "48h",
              "covers": ["Submitting this essay up to 48 hours late, with no penalty"],
              "excludes": ["Exams and in-class quizzes", "Group project deadlines"] }
          ],
          "log": [
            { "id": "sa-1", "kind": "late", "by": "Samuel Adeyemi", "at": "2026-09-09T21:40:00", "reason": "Flu, doctor's note sent to the TA", "target": "Reading response 2", "status": "used" }
          ]
        }</script>
      </mv-pass-card>
      <p class="pc-note">Your instructor sees every late pass and its reason.</p>
    </section>

    <!-- Card, exhausted: spending-limit overrides need a manager's approval -->
    <section class="pc-panel" aria-label="Expense">
      <p class="pc-kicker">Company card · Elena Petrova</p>
      <div class="pc-expense">
        <strong>Figma Organization · annual plan</strong>
        <span>$1,440.00</span>
      </div>
      <mv-pass-card id="pc-limit" label="Limit overrides" total="2" period="this month" refills="2026-10-01"
        blocked rule="This purchase is over your $1,000.00 per-transaction limit" action="Figma Organization · $1,440.00"
        user="Elena Petrova" require-approval requestable log-limit="2">
        <script type="application/json">{
          "approvers": ["Tomás Rivera · Engineering manager"],
          "kinds": [{ "id": "limit", "label": "One purchase over limit", "covers": ["This one purchase, up to $5,000.00"], "excludes": ["Recurring charges after the first one", "Cash withdrawals"] }],
          "log": [
            { "id": "ep-2", "kind": "limit", "by": "Elena Petrova", "at": "2026-09-16T10:12:00", "reason": "Booth deposit for DevWorld Berlin, due today", "target": "DevWorld Berlin · $1,250.00", "status": "used", "approver": "Tomás Rivera" },
            { "id": "ep-1", "kind": "limit", "by": "Elena Petrova", "at": "2026-09-03T16:48:00", "reason": "Reserved instances renewal before the price change", "target": "AWS · $2,180.00", "status": "used", "approver": "Tomás Rivera" }
          ]
        }</script>
      </mv-pass-card>
    </section>
  </div>

  <script type="module">
    const main = document.getElementById("pc-main");
    const merge = document.getElementById("pc-merge");
    const mergeNote = document.getElementById("pc-merge-note");
    const badge = document.getElementById("pc-freeze-badge");
    const fail = document.getElementById("pc-fail");
    const approveBtn = document.getElementById("pc-approve");
    const out = document.getElementById("pc-log");
    const say = (text) => { out.textContent = text; };

    // Relative dates keep the main log recent whenever the demo is opened.
    const ago = (days, hh, mm) => { const t = new Date(); t.setDate(t.getDate() - days); t.setHours(hh, mm, 0, 0); return t.toISOString(); };
    const initialLog = () => [
      { id: "fz-2", kind: "freeze", by: "Aiko Tanaka", at: ago(2, 16, 5), target: "PR #4802 · Update currency rounding tables", reason: "Needed the new tables before the Tokyo launch. Turned out the fix landed before the freeze, so I handed it back.", status: "returned" },
      { id: "fz-1", kind: "freeze", by: "Daniel Okafor", at: ago(6, 11, 42), target: "PR #4790 · Patch image upload validation", reason: "Security fix for the image upload endpoint. Waiting 9 days for the freeze to lift would leave it exposed.", status: "used" },
    ];
    main.log = initialLog();

    const sync = () => {
      const state = main.state;
      const covered = state === "covered";
      merge.setAttribute("aria-disabled", String(!covered));
      mergeNote.textContent = covered ? "A freeze pass covers this merge." : state === "pending" ? "Waiting for the release captain to approve the hotfix pass." : "Merging is blocked by the release freeze.";
      badge.textContent = covered ? "Pass in effect" : state === "pending" ? "Awaiting approval" : "Blocking";
      badge.dataset.variant = covered ? "success" : state === "pending" ? "warning" : "danger";
      approveBtn.setAttribute("aria-disabled", String(!main.log.some((e) => e.status === "pending")));
    };
    customElements.whenDefined("mv-pass-card").then(sync);

    // A fake server that records each pass (and can refuse it).
    const server = (ms = 700) => new Promise((resolve, reject) => {
      const bad = fail.checked;
      setTimeout(() => (bad ? reject(new Error("503")) : resolve({ id: `srv-${Math.random().toString(36).slice(2, 7)}` })), ms);
    });

    for (const el of document.querySelectorAll("#pc-demo mv-pass-card")) {
      el.addEventListener("mv-pass-use", (e) => {
        e.detail.waitUntil(server());
        say(`mv-pass-use · ${e.detail.kind.label} · ${e.detail.remaining} left after`);
      });
      el.addEventListener("mv-pass-return", (e) => {
        e.detail.waitUntil(server(400));
        say(`mv-pass-return · ${e.detail.remaining} left after`);
      });
      el.addEventListener("mv-pass-request", (e) => {
        e.detail.waitUntil(server(600));
        say(`mv-pass-request · ${e.detail.remaining} of ${e.detail.total} left`);
      });
      el.addEventListener("mv-pass-error", (e) => {
        fail.checked = false;
        say(`mv-pass-error · ${e.detail.action} failed (${e.detail.error.message}), nothing was spent`);
      });
    }
    main.addEventListener("mv-pass-change", (e) => { sync(); say(`mv-pass-change · ${e.detail.reason} · ${e.detail.remaining} of ${e.detail.total} left`); });

    merge.addEventListener("click", () => {
      if (merge.getAttribute("aria-disabled") === "true") return;
      say("Merged #4821 into main under a freeze pass.");
    });
    approveBtn.addEventListener("click", () => {
      const pending = main.log.find((e) => e.status === "pending");
      if (pending) main.approve(pending.id, { by: "Priya Nair" });
    });
    document.getElementById("pc-reset").addEventListener("click", () => {
      main.close();
      main.log = initialLog();
      main.requested = false;
      fail.checked = false;
      sync();
      say("Demo reset.");
    });
  </script>
</div>

Referência cultural

Monopoly, Parker Brothers (a partir de The Landlord's Game, de Lizzie Magie) (1935, jogo). A carta “Saia livre da prisão” é uma isenção rara e física de uma regra: você pode guardá-la até o momento em que ela realmente importa, ela liberta você exatamente uma vez, e depois precisa voltar ao baralho. Aqui ela se torna uma carteira visível com algumas exceções a regras por período, cada uma usada de forma deliberada com um motivo declarado, limitada ao que cobre, registrada para os revisores e devolvida se não for usada.

API

Attributes

NameTipoDefaultDescription
totalnumber3Passes in the budget for the current period. Passes in effect, awaiting approval or used count against it; returned and declined ones do not.
labelstringPassesWallet title (“Freeze passes”, “Late passes”, “Limit overrides”).
periodstringthis periodHow the period reads after the count: “2 of 3 left this quarter”.
refillsdate (ISO, a bare YYYY-MM-DD is local)When the budget refills, shown as “Refills Oct 1” in the wallet, the gate and the empty state. Display only: your app resets the log when the period changes.
blockedbooleanA rule currently blocks the action: shows the gate with “Use a pass”. The gate reads “Pass in effect” or “Waiting for approval” while a pass for the same action is active or pending.
rulestringThe rule that blocks, as the gate title (“Merging to main is frozen until Oct 3 for the 4.2 release”). Children with slot="rule" replace it with rich content.
actionstringWhat is blocked (“PR #4821 · Fix rounding in checkout totals”). Stored as the target of the pass; a pass in effect covers the gate only when its target matches.
userstringYouName recorded as “who” in the log. Only this user’s passes in effect or pending show Return / Withdraw.
require-approvalbooleanEvery pass needs an approver (a kind can also require it with approval: true). The approver select becomes required and the pass is held until approve().
min-reasonnumber15Minimum reason length in characters (at least 1: a reason is always required).
requestablebooleanShows “Request more” in the wallet (and in the gate once no pass is left).
requestedbooleanSet by the component once a request is sent (the button reads “Requested” and is disabled). Remove it to allow another request.
variant"card" | "inline"cardcard: wallet, gate and log in one card. inline: only the gate (with the count in its hint), to sit in a list row or next to a blocked button.
hide-logbooleanHides the pass log (always hidden in the inline variant).
log-limitnumber4Rows shown before “Show all N”.
localeBCP 47 tagen-USLocale for dates and times (Intl.DateTimeFormat).
data-stateavailable | empty | blocked | covered | pendingSet by the component (styleable). data-empty when no pass is left, data-open while the form is open, data-busy while a waitUntil promise is pending.

Properties

NameTipoDescription
kindsArray<{ id, label, description?, covers?: string[], excludes?: string[], duration?: "24h" | "48h" | "7d" | ms, limit?: number, approval?: boolean }>Pass types. With several, the form shows them as radio cards; limit caps that kind per period (“1 of 1 left”, disabled at 0); duration sets how long a pass stays in effect once used (none = one action). Also settable from the JSON child.
logArray<{ id, kind, by, at, reason, target?, status: "active" | "pending" | "used" | "returned" | "denied", approver?, until? }>Uses this period (dates as ISO strings, Date or epoch ms). Read it to persist; an active pass past its until reads as used. Also settable from the JSON child.
approversstring[]Names offered in the approver select.
remainingnumberPasses left this period (read-only).
usednumberPasses spent this period: in effect, awaiting approval or used (read-only).
state"available" | "empty" | "blocked" | "covered" | "pending"Current gate state, or wallet state when nothing is blocked (read-only).
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (English defaults: title, use, useFor, afterUse, noneLeft, covers, excludes, validFor, reason, reasonHint, reasonShort, approver, summary, summaryHeld, accountable, confirm, confirmApproval, coveredTitle, coveredUntil, pendingTitle, pendingText, logTitle, logNote, status*, announce*, …).

Methods

NameDescription
use({ kind?, reason, approver?, target? })Uses a pass programmatically (same path as the form, including the cancelable mv-pass-use and waitUntil). Resolves to the logged entry, or null if refused, cancelled, failed or no pass is left.
returnPass(id)Hands back a pass in effect or awaiting approval (cancelable mv-pass-return): it is marked returned and no longer counts. Resolves true when done.
approve(id, { by? })Settles a pending pass: it takes effect now for its kind’s duration. Call it when your server confirms the approval.
deny(id, { by?, note? })Settles a pending pass as declined: it is not counted.
request()Asks for more passes (cancelable mv-pass-request with waitUntil), then sets requested. Resolves true once sent.
open() / close()Opens or closes the “Use a pass” form (open only while the gate is blocked and a pass is left).

Events

NameDescription
mv-pass-useCancelable, before a pass is spent. detail: { entry, kind, reason, approver, target, remaining (after use), total, waitUntil(promise) }. Call waitUntil() synchronously with your request: the confirm button shows “Logging…”, a rejection spends nothing and shows an error, an object it resolves to is merged into the entry (server id, until…). preventDefault() refuses the use.
mv-pass-returnCancelable, before a pass is handed back. detail: { entry, remaining (after return), total, waitUntil(promise) }.
mv-pass-requestCancelable, “Request more” was chosen. detail: { remaining, total, target, waitUntil(promise) }.
mv-pass-changeThe log changed. detail: { reason: "use" | "return" | "approve" | "deny" | "expire", entry, log, remaining, total }. Use it to persist.
mv-pass-errorA waitUntil promise rejected. detail: { action: "use" | "return" | "request", error }.

Content structure

NameDescription
ruleRich content (a link to the freeze calendar, a policy name…) shown as the gate title instead of the rule attribute while the action is blocked.

CSS classes

NameDescription
mv-pass-card-walletWallet: -head, -mark, -title, -summary, -request, -meter, -count (-count-value, -count-of), -legend.
mv-pass-card-ticketOne ticket per pass in .mv-pass-card-tickets, data-state available | pending | active | used; -ticket-glyph holds its icon. data-dense on the strip above 10 passes.
mv-pass-card-gateBlocked-action strip, data-state blocked | empty | covered | pending: -gate-icon, -gate-title, -rule (slot), -gate-text, -gate-hint, -use, -gate-return, -gate-request.
mv-pass-card-formUse form: -form-title, -kinds (mv-choice-group), -scope (-scope-col[data-scope=covers|excludes], -scope-list, -validity), -reason-field, -approver, -after (-pip[data-state]), -form-error, -form-actions, -confirm.
mv-pass-card-logLog: -log-title, -log-note, -log-toggle, -table (rows with data-status; cells with data-col), -status[data-status], -status-sub, -row-return. Rows stack as cards under a 40rem container width.

CSS variables

NameDefaultDescription
--mv-pass-card-ticketvar(--mv-accent)Color of available tickets, the wallet mark and the after-use pips.
--mv-pass-card-activevar(--mv-success)Pass in effect: ticket, covered gate, status pill.
--mv-pass-card-heldvar(--mv-warning)Pass awaiting approval: striped ticket, pending gate, status pill.
--mv-pass-card-blockedvar(--mv-danger)Tint of the blocked gate and of declined passes.
--mv-pass-card-spentvar(--mv-fg-subtle)Used tickets and neutral status pills.

Accessibility

Counts are always text: the wallet is a labelled section whose description reads “2 of 3 left this quarter · Refills Oct 1”, and the legend names each state in words (“1 in effect · 1 used”); the ticket strip and after-use pips are decorative (aria-hidden) and states also differ by icon and border style (bolt, stripes, hollow dashed and punched), never by color alone. The gate is a labelled group; “Use a pass” is a real button with aria-expanded / aria-controls, described by the hint (“1 of 3 left after this”, or “No passes left this quarter. Refills Oct 1.”). With no pass left it stays focusable with aria-disabled so the reason can be read. The form is a native <form> (novalidate) labelled by its title: pass types are native radios in a fieldset with a legend, covers / does-not-cover are headed lists, the reason is a <textarea> with a visible label, required and minlength, a hint and an error message linked through aria-describedby with aria-invalid; the approver is a labelled, required <select>. Submitting with a missing field focuses the first invalid control. Escape closes the form and returns focus to “Use a pass”; Ctrl/Cmd+Enter submits from the reason. While a waitUntil promise is pending the confirm button is aria-busy and aria-disabled. After a pass is used, focus moves to the gate title (“Pass in effect”) and a polite live region announces “Pass used for PR #4821. 1 of 3 left this quarter. Logged for reviewers.”; returns, approvals and declines are announced the same way, failures assertively. The log is a real table with a caption-like heading (aria-labelledby), column headers and <time datetime> cells; Return / Withdraw buttons carry a full label (“Return the pass used Sep 24, 3:40 PM for PR #4821”). Under 40rem the rows stack as cards with explicit table roles so the semantics survive. Motion (ticket flip, pulsing next pip, new-row highlight) is off under prefers-reduced-motion or data-motion="reduce"; forced-colors mode keeps borders and states visible.

Esta página foi traduzida com IA. Informar um problema de tradução