ExklusivBeta

Unclaimed <mv-unclaimed>

Ownership-Radar für gemeinsame Arbeitslisten: Umschließe eine beliebige Liste, Tabelle oder ein Board mit Dingen, die alle für wichtig halten (Action Items aus Meetings, Nacharbeiten zu Incidents, Retro-Aktionen, Bug-Triage, Threads im geteilten Postfach, Alerts ohne Bereitschaft, Dokumentationsschulden), und jedes Element mit leerem data-owner bekommt eine ruhige Markierung „Nobody yet“, einen leeren gestrichelten Platz, wo ein Owner sitzen würde, der mit zunehmendem Alter sanft präsenter wird (Tönung, Rahmen und Text festigen sich über das Reife-Fenster; nie rot, nie animiert). Ein an die Liste gehefteter Übersichts-Chip liest „5 things nobody owns · oldest 18 days · 2 gone quiet“ und öffnet ein nach Alter oder Wirkung sortiertes Panel, in dem jeder ein Element mit einem Klick übernehmen kann (abbrechbares mv-claim mit waitUntil, die App speichert), jemanden aus Kandidaten vorschlagen kann, die die App liefert, samt angezeigter Begründung („Maya Chen · Owns payments · 3 open tasks“; das Element bleibt offen, markiert mit „Asked Maya“, bis sie annimmt), oder es mit Grund und Notiz als nicht benötigt archivieren kann. Übernommene Elemente, die ihr Owner seit dem stale-Fenster nicht angefasst hat, bilden eine sanftere zweite Ebene mit Take over, Check in und, für deine eigenen, Still on it. Funktioniert mit vorhandenem Markup (data-owner, data-since, data-touched, data-impact) oder rendert aus einem items-Array; Übernahmen von anderswo (Echtzeit, ein anderes Gerät) werden erkannt, kurz hervorgehoben und angesagt.

KategorieDatenanzeige
TypWeb Component (<mv-unclaimed>)
StatusBeta
Installiert auchbutton
Keywordsexclusive, culture, ownership, owner, assignee, unassigned, action-items, follow-ups, triage, retro, incident, shared-inbox, stale, accountability, volunteer, suggest, list, board

When to use

  • A meeting, retro or incident review produces action items and some quietly never get an owner
  • A shared inbox, triage queue or alert list needs to show which entries nobody has picked up, and since when
  • A team wants a low-pressure way to volunteer, suggest the right person or drop work nobody needs anymore
  • Items that were claimed but abandoned should resurface gently without shaming anyone

Avoid when

  • Work is assigned by a lead and moves through stages; the need is a workflow board, not finding owners → use Kanban instead
  • One person processes a queue one item at a time with keyboard decisions → use Hold Slot instead
  • People sign up for slots toward a shared goal (who brings what, fundraising, shift coverage) → use Stone Soup instead

Installation

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

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["unclaimed"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/unclaimed/unclaimed.js, components/unclaimed/unclaimed.css, components/button/button.css.

Verwendung

Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:

<div id="uc-demo" style="width:min(100%,62rem);margin-inline:auto">
  <style>
    #uc-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1.25rem; align-content:start }
    #uc-demo .uc-card { display:block; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #uc-demo .uc-head { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:1rem 1.125rem .875rem; border-bottom:1px solid var(--mv-border) }
    #uc-demo .uc-head h3 { margin:0; font-size:1rem; letter-spacing:-.01em }
    #uc-demo .uc-head p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #uc-demo .uc-head > div { min-width:0; max-width:100% }
    #uc-demo .uc-list { margin:0; padding:0; list-style:none }
    #uc-demo .uc-item { display:grid; grid-template-columns:auto minmax(0,1fr) minmax(11rem,auto); align-items:center; gap:.875rem; padding:.625rem .75rem }
    #uc-demo .uc-item + .uc-item { border-top:1px solid var(--mv-border) }
    #uc-demo .uc-item[data-unclaimed="unclaimed"] { background:color-mix(in oklab, var(--mv-warning) calc(3% * var(--mv-unclaimed-age, 0)), transparent) }
    #uc-demo .uc-box { width:1rem; height:1rem; border:1.5px solid var(--mv-border-strong); border-radius:var(--mv-radius-xs) }
    #uc-demo .uc-main { display:grid; gap:.125rem; min-width:0 }
    #uc-demo .uc-main b { font-size:.875rem; font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #uc-demo .uc-main span { color:var(--mv-fg-muted); font-size:.75rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #uc-demo .uc-who { display:flex; align-items:center; justify-content:flex-end; gap:.5rem; min-width:0; font-size:.8125rem; color:var(--mv-fg-muted) }
    #uc-demo .uc-av { display:grid; place-items:center; flex:none; width:1.5rem; height:1.5rem; border-radius:50%; background:var(--mv-bg-emphasis); color:var(--mv-fg); font-size:.625rem; font-weight:600 }
    #uc-demo .uc-av:empty, #uc-demo [data-unclaimed-owner]:empty { display:none }
    #uc-demo .uc-foot { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; padding:.75rem 1.125rem; border-top:1px solid var(--mv-border); background:var(--mv-bg-subtle); border-radius:0 0 var(--mv-radius-xl) var(--mv-radius-xl) }
    #uc-demo .uc-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:.75rem/1.4 var(--mv-font-mono); flex:1 1 18rem }
    #uc-demo .uc-actions { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #uc-demo .mv-choice { font-size:.8125rem }
    #uc-demo .uc-row2 { display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,1fr); gap:1.25rem; align-items:start }
    #uc-demo .uc-pad { padding:1rem 1.125rem 1.125rem }
    #uc-demo .uc-pad > h4 { margin:0; font-size:.9375rem; letter-spacing:-.01em }
    #uc-demo .uc-pad > p { margin:.125rem 0 .875rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #uc-demo .uc-board { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.75rem }
    #uc-demo .uc-col { display:grid; gap:.5rem; align-content:start; padding:.625rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle) }
    #uc-demo .uc-col h5 { margin:0 0 .125rem; color:var(--mv-fg-muted); font-size:.6875rem; font-weight:600; letter-spacing:.04em; text-transform:uppercase }
    #uc-demo .uc-note { display:grid; gap:.5rem; padding:.625rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-surface); font-size:.8125rem; line-height:1.4 }
    #uc-demo .uc-note footer { display:flex; align-items:center; gap:.375rem; flex-wrap:wrap; min-height:1.5rem; color:var(--mv-fg-muted); font-size:.75rem }
    @media (max-width:48rem) {
      #uc-demo .uc-row2 { grid-template-columns:minmax(0,1fr) }
      #uc-demo .uc-item { grid-template-columns:auto minmax(0,1fr) }
      #uc-demo .uc-who { grid-column:2; justify-content:flex-start }
    }
    @media (max-width:32rem) { #uc-demo .uc-board { grid-template-columns:minmax(0,1fr) } }
  </style>

  <!-- 1 · Incident follow-ups: declarative list, summary chip in the header, candidates from the app -->
  <mv-unclaimed id="uc-main" class="uc-card" me="Priya Raman" now="2026-09-24T15:00:00Z" noun="follow-up" stale="7d">
    <header class="uc-head">
      <div>
        <h3>Incident follow-ups</h3>
        <p>Action items from the last four incident reviews · Platform team</p>
      </div>
      <div data-unclaimed-summary></div>
    </header>
    <ul class="uc-list" aria-label="Follow-ups">
      <li class="uc-item" data-owner="" data-since="2026-09-06T10:00:00Z" data-impact="low" data-area="docs" data-context="Partner API review · Sep 5">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Document the new rate limits for partners</b><span>Partner API review · Sep 5</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner></span></span>
      </li>
      <li class="uc-item" data-owner="" data-since="2026-09-13T09:00:00Z" data-impact="high" data-area="payments" data-context="Payment webhook outage · Sep 12">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Add alerting on the webhook retry backlog</b><span>Payment webhook outage · Sep 12</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner></span></span>
      </li>
      <li class="uc-item" data-owner="Maya Chen" data-since="2026-09-13T09:00:00Z" data-touched="2026-09-23T16:00:00Z" data-impact="high" data-area="payments" data-context="Payment webhook outage · Sep 12">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Add a retry budget to the payment client</b><span>Payment webhook outage · Sep 12</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner>Maya Chen</span></span>
      </li>
      <li class="uc-item" data-owner="" data-since="2026-09-15T14:00:00Z" data-impact="medium" data-area="database" data-context="Database failover drill · Sep 15">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Write the runbook for failing over the read replica</b><span>Database failover drill · Sep 15</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner></span></span>
      </li>
      <li class="uc-item" data-owner="Amara Okafor" data-since="2026-09-06T10:00:00Z" data-touched="2026-09-10T11:00:00Z" data-impact="medium" data-area="qa" data-context="Refund incident · Sep 4">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Fix the flaky end-to-end test for partial refunds</b><span>Refund incident · Sep 4</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner>Amara Okafor</span></span>
      </li>
      <li class="uc-item" data-owner="" data-since="2026-09-22T08:30:00Z" data-impact="high" data-area="security" data-context="Payment webhook outage · Sep 12">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Rotate the staging API keys that were pasted in chat</b><span>Payment webhook outage · Sep 12</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner></span></span>
      </li>
      <li class="uc-item" data-owner="Priya Raman" data-since="2026-09-15T14:00:00Z" data-touched="2026-09-15T17:00:00Z" data-impact="low" data-area="docs" data-context="Database failover drill · Sep 15">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Update the status page templates for partial outages</b><span>Database failover drill · Sep 15</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner>Priya Raman</span></span>
      </li>
      <li class="uc-item" data-owner="" data-since="2026-09-23T11:00:00Z" data-impact="low" data-area="frontend" data-suggested="Kenji Watanabe" data-context="Checkout latency review · Sep 22">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Remove the feature flag for the old checkout</b><span>Checkout latency review · Sep 22</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner></span></span>
      </li>
      <li class="uc-item" data-owner="Lucas Moreau" data-since="2026-09-15T14:00:00Z" data-touched="2026-09-21T10:00:00Z" data-impact="medium" data-area="database" data-context="Database failover drill · Sep 15">
        <span class="uc-box" aria-hidden="true"></span>
        <span class="uc-main"><b>Load-test the new queue consumer at 3× peak</b><span>Database failover drill · Sep 15</span></span>
        <span class="uc-who" data-unclaimed-slot><span class="uc-av" aria-hidden="true"></span><span data-unclaimed-owner>Lucas Moreau</span></span>
      </li>
    </ul>
    <div class="uc-foot">
      <p class="uc-log" id="uc-log">Signed in as Priya Raman. Open the chip to take, suggest or archive a follow-up.</p>
      <span class="uc-actions">
        <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="uc-fail"> Make saving fail</label>
        <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="uc-remote">A teammate claims one</button>
        <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="uc-reset">Reset</button>
      </span>
    </div>
  </mv-unclaimed>

  <div class="uc-row2">
    <!-- 2 · Retro board: any markup (cards in columns), default chip placement, no suggestions -->
    <section class="uc-card uc-pad" aria-labelledby="uc-retro-title">
      <h4 id="uc-retro-title">Sprint 42 retro · actions</h4>
      <p>Everyone voted for these. Owners are optional on the board.</p>
      <mv-unclaimed id="uc-retro" me="Priya Raman" now="2026-09-24T15:00:00Z" noun="action" stale="10d" reasons="Already done, Duplicate, Not worth it now">
        <div class="uc-board">
          <div class="uc-col">
            <h5>Process</h5>
            <article class="uc-note" data-owner="" data-since="2026-09-12T16:00:00Z" data-impact="high">
              <span>Agree on a definition of done for design reviews</span>
              <footer data-unclaimed-slot></footer>
            </article>
            <article class="uc-note" data-owner="Sofia Rossi" data-since="2026-09-12T16:00:00Z" data-touched="2026-09-22T09:00:00Z">
              <span>Move the planning meeting to Tuesday mornings</span>
              <footer data-unclaimed-slot><span class="uc-av" aria-hidden="true">SR</span>Sofia Rossi</footer>
            </article>
          </div>
          <div class="uc-col">
            <h5>Tooling</h5>
            <article class="uc-note" data-owner="" data-since="2026-09-19T16:00:00Z" data-impact="medium">
              <span>Cache dependencies in CI to get builds under 8 minutes</span>
              <footer data-unclaimed-slot></footer>
            </article>
            <article class="uc-note" data-owner="" data-since="2026-09-23T16:00:00Z" data-impact="low">
              <span>Add a pull request template with a testing checklist</span>
              <footer data-unclaimed-slot></footer>
            </article>
          </div>
        </div>
      </mv-unclaimed>
    </section>

    <!-- 3 · Shared inbox: rendered from the items property, chip at the bottom -->
    <section class="uc-card uc-pad" aria-labelledby="uc-inbox-title">
      <h4 id="uc-inbox-title">[email protected]</h4>
      <p>Shared inbox · rendered from data with the <code>items</code> property.</p>
      <mv-unclaimed id="uc-inbox" me="Priya Raman" now="2026-09-24T15:00:00Z" noun="thread" stale="5d" placement="bottom"></mv-unclaimed>
    </section>
  </div>

  <script type="module">
    const main = document.getElementById("uc-main");
    const inbox = document.getElementById("uc-inbox");
    const log = document.getElementById("uc-log");
    const fail = document.getElementById("uc-fail");
    const say = (text) => { log.textContent = text; };
    const initialsOf = (name) => name.split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();

    // Owner avatars in the demo list follow data-owner.
    const paint = () => {
      for (const li of main.querySelectorAll(".uc-item")) {
        const av = li.querySelector(".uc-av");
        const owner = li.getAttribute("data-owner") || "";
        av.textContent = owner ? initialsOf(owner) : "";
      }
    };
    paint();
    const snapshot = [...main.querySelectorAll(".uc-item")].map((li) => ({
      li, owner: li.getAttribute("data-owner"), touched: li.getAttribute("data-touched"), suggested: li.getAttribute("data-suggested"),
    }));

    // A fake API: 500 ms round trip, optionally failing.
    const save = () => new Promise((resolve, reject) => setTimeout(() => (fail.checked ? reject(new Error("503")) : resolve()), 500));

    // Suggestions come from the app: expertise first, then the lightest load, with the reason shown.
    const team = [
      { name: "Maya Chen", areas: ["payments"], owns: "Owns payments", open: 3 },
      { name: "Lucas Moreau", areas: ["database"], owns: "Owns the database", open: 2 },
      { name: "Kenji Watanabe", areas: ["frontend"], owns: "Built the new checkout", open: 1 },
      { name: "Sofia Rossi", areas: ["security"], owns: "Security champion", open: 4 },
      { name: "Amara Okafor", areas: ["qa"], owns: "Maintains the test suite", open: 6 },
      { name: "Elena Petrova", areas: ["docs"], owns: "Wrote the partner API docs", open: 2 },
    ];
    main.candidates = (item) => new Promise((resolve) => {
      const area = item.element.dataset.area;
      const load = (n) => `${n} open task${n === 1 ? "" : "s"}`;
      const ranked = [...team]
        .filter((p) => p.name !== item.suggested)
        .sort((a, b) => Number(b.areas.includes(area)) - Number(a.areas.includes(area)) || a.open - b.open)
        .slice(0, 3)
        .map((p) => ({ name: p.name, reason: p.areas.includes(area) ? `${p.owns} · ${load(p.open)}` : `Lightest load · ${load(p.open)}` }));
      setTimeout(() => resolve(ranked), 450);
    });

    main.addEventListener("mv-claim", (e) => {
      const { item, via, previousOwner } = e.detail;
      say(via === "takeover" ? `Taking over from ${previousOwner}…` : via === "renew" ? "Updating…" : `Assigning “${item.title}” to you…`);
      e.detail.waitUntil(save());
    });
    main.addEventListener("mv-suggest", (e) => {
      say(e.detail.nudge ? `Pinging ${e.detail.candidate.name}…` : `Asking ${e.detail.candidate.name}…`);
      e.detail.waitUntil(save());
    });
    main.addEventListener("mv-dismiss", (e) => {
      say(`Archiving (${e.detail.reason})…`);
      e.detail.waitUntil(save());
    });
    main.addEventListener("mv-error", () => say("The server said no (503). Nothing changed."));
    main.addEventListener("mv-change", (e) => {
      paint();
      const { action, item } = e.detail;
      const msg = {
        claim: `${item.owner} owns “${item.title}”.`,
        renew: `“${item.title}” marked as still in progress.`,
        suggest: `Suggested to ${e.detail.candidate?.name}. It stays open until they accept.`,
        "check-in": `Check-in sent to ${item.owner}.`,
        dismiss: `Archived: ${e.detail.reason}.`,
      }[action];
      if (msg) say(msg);
    });

    document.getElementById("uc-remote").addEventListener("click", () => {
      const open = [...main.querySelectorAll('.uc-item[data-owner=""]')];
      const li = open[open.length - 1];
      if (!li) return say("Everything already has an owner.");
      li.setAttribute("data-owner", "Lucas Moreau");
      li.setAttribute("data-touched", "2026-09-24T15:00:00Z");
      li.removeAttribute("data-suggested");
      li.querySelector("[data-unclaimed-owner]").textContent = "Lucas Moreau";
      paint();
      say("Lucas Moreau took one from another device.");
    });

    document.getElementById("uc-reset").addEventListener("click", () => {
      main.close();
      for (const s of snapshot) {
        s.li.setAttribute("data-owner", s.owner);
        s.touched ? s.li.setAttribute("data-touched", s.touched) : s.li.removeAttribute("data-touched");
        s.suggested ? s.li.setAttribute("data-suggested", s.suggested) : s.li.removeAttribute("data-suggested");
        s.li.removeAttribute("data-dismissed");
        s.li.hidden = false;
        s.li.style.opacity = "";
        s.li.getAnimations().forEach((a) => a.cancel());
        s.li.querySelector("[data-unclaimed-owner]").textContent = s.owner;
      }
      paint();
      say("Demo reset.");
    });

    inbox.items = [
      { id: "t-4821", title: "Data export is missing March transactions", context: "Lena Fischer · Billing", since: "2026-09-23T13:00:00Z", impact: "high" },
      { id: "t-4807", title: "Refund stuck in review for order #48213", context: "Javier Alvarez · Payments", since: "2026-09-21T09:00:00Z", impact: "medium" },
      { id: "t-4799", title: "SSO login loops after a browser update", context: "Aiko Tanaka · Access", owner: "Kenji Watanabe", since: "2026-09-20T10:00:00Z", touched: "2026-09-23T17:00:00Z" },
      { id: "t-4776", title: "Partner asks for sandbox API keys", context: "Omar Haddad · Partners", owner: "Sofia Rossi", since: "2026-09-15T08:00:00Z", touched: "2026-09-17T12:00:00Z" },
      { id: "t-4760", title: "Can invoices be issued in EUR?", context: "Chloé Martin · Billing", since: "2026-09-18T15:00:00Z", impact: "low" },
    ];
  </script>
</div>

Kulturelle Referenz

Der Katze die Schelle umhängen, Äsop (um 600 v. Chr., Fabel). Die Mäuse sind sich alle einig, dass jemand der Katze eine Schelle umhängen sollte, doch auf die Frage, wer es tun wird, meldet sich niemand, und die gute Idee bleibt unerledigt. In der UI wird Arbeit, die alle für wichtig halten, die aber niemandem gehört, dort sichtbar gemacht, wo sie liegt: mit einer ruhigen Markierung, die mit dem Alter wächst, einem an die Liste gehefteten Zähler und einer Möglichkeit, sich mit einem Klick freiwillig zu melden, jemanden zu benennen oder sie fallen zu lassen.

API

Attributes

NameTypDefaultDescription
item-selectorCSS selector[data-owner]Which descendants are items. Each item reads data-owner (empty = nobody), data-since (ISO date or epoch ms: when it was raised), data-touched (last owner activity, defaults to data-since), data-impact (high | medium | low, or 1-3), data-title (else the first heading, strong, b or link, else its text), data-context (shown in the panel), data-suggested (a name already asked) and data-dismissed (archived; hidden). data-id or id identifies it in events.
mestringYouName of the current user: the owner written by “I’ll take it” and Take over, and the owner whose stale items get Still on it instead of Take over.
staletime ("7d", "36h", "2w"; bare number = days; "off")7dA claimed item whose data-touched is older than this joins the “Claimed, but gone quiet” tier and gets a neutral “Quiet” marker. off disables the tier.
maturetime14dAge at which an unclaimed marker reaches full presence. In between, --mv-unclaimed-age goes 0 → 1 and data-unclaimed-age steps through fresh, settling, lingering, long.
sortage | impactageOrder of the panel: oldest first, or highest impact first (then oldest). Reflects the toggle in the panel header.
noun / noun-pluralstringthing / thingsWhat the items are called in the chip and announcements (“3 follow-ups nobody owns”). With the default noun the clear state reads “Everything has an owner”.
placementtop | bottomtopWhere the sticky summary bar sits when no [data-unclaimed-summary] host is present inside the element.
reasonscomma-separated listAlready done, Duplicate, Out of scope, Won’t doReasons offered by “Not needed” (radio chips, plus an optional free-text note).
nowISO date | epoch msReference time for ages. Defaults to the current time (ages refresh every minute); set it for server-rendered consistency, screenshots or tests. The now property also accepts a Date.
markers"true" | "false"truefalse keeps the summary and panel but inserts no marker in the items (style them yourself from data-unclaimed and --mv-unclaimed-age).
data-stateopen | quiet | clearSet by the component on itself and on the chip: some items have no owner, only quiet ones remain, or everything is owned.

Properties

NameTypDescription
itemsArray<{ id, title, owner?, since?, touched?, impact?, context?, href?, suggested?, dismissed? }> | nullRender the list from data instead of markup (a ul.mv-unclaimed-list with owner avatars). Claims, suggestions and dismissals are written back to these objects; read the property to get them.
candidatesArray<Candidate> | (item) => Candidate[] | Promise<Candidate[]>Who “Suggest someone” proposes. Candidate: { name, reason? (string or string[] joined with ·), id?, avatar? (image URL) }. Called once per item when its drawer opens (a loading state shows while the promise is pending). Without it the Suggest button is not shown.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (nobody, asked, quiet, takenYou, taken, summary, summaryOldest, summaryQuiet, summaryClear, panelTitle, claim, suggest, dismiss, takeOver, checkIn, renew, suggestTitle, dismissTitle, reasons, archive, cancel, announceClaim, announceOther, announceSuggest, announceDismiss, announceRest, announceError…). English defaults with {count}, {noun}, {age}, {title}, {name}, {owner} placeholders.
formatAge(ms, "long" | "short") => stringLocalize ages (“9 days” / “9d”). Default: hours, days, weeks after 14 days, months after 60.
summary{ unclaimed, quiet, oldest, items }Current tallies and every item’s public detail (read-only).
expandedbooleanWhether the panel is open (read-only).

Methods

NameDescription
claim(item, owner?)Claims an item (id, element or detail) for owner (default me); a stale item owned by someone else is a takeover. Goes through mv-claim. Resolves true when applied.
suggest(item, candidate)Marks an item as suggested to a candidate ({ name } or a name) through mv-suggest. Resolves true when applied.
dismiss(item, reason?, note?)Archives an item through mv-dismiss (data-dismissed + hidden, or dismissed on the items object).
open() / close() / toggle()Opens or closes the panel.
refresh()Re-reads the items now. Markup and attribute changes are already picked up by a MutationObserver.

Events

NameDescription
mv-claimCancelable, before an item gets an owner or is renewed. detail: { item, owner, previousOwner, via: "self" | "takeover" | "renew" | "api", waitUntil(promise) }. Call waitUntil() with your save request: the button shows Saving…, a rejection keeps everything as it was and shows an error; preventDefault() refuses the claim.
mv-suggestCancelable, when a candidate is picked (nudge false: the item is marked data-suggested) or when Check in is pressed on a quiet item (nudge true, candidate = current owner). detail: { item, candidate, nudge, waitUntil(promise) }. Send the notification from here.
mv-dismissCancelable, before an item is archived as not needed. detail: { item, reason, note, waitUntil(promise) }.
mv-changeAfter a change was applied. detail: { action: "claim" | "renew" | "suggest" | "check-in" | "dismiss", item, candidate?, reason?, note? }. Re-render owner avatars or counters from here.
mv-errorA waitUntil promise rejected; nothing changed. detail: { action, item, error }.
mv-open / mv-closeThe panel opened or closed.

Content structure

NameDescription
[data-unclaimed-summary]Any element inside mv-unclaimed with this attribute (a list header, a toolbar) receives the summary chip instead of the sticky bar.
[data-unclaimed-slot]Inside an item: where its marker goes (default: the item itself, or the last cell of a table row).
[data-unclaimed-owner]Inside an item: its textContent is set to the new owner after a claim.
[data-unclaimed-action]A button inside an item with claim, suggest, dismiss or open runs that action for its item (suggest and dismiss open the panel on that item’s drawer).

CSS classes

NameDescription
mv-unclaimed-bar / mv-unclaimed-summarySticky bar and the chip button: -summary-seats (up to three empty dashed seats), -summary-count, -summary-label, -summary-oldest, -summary-quiet, -summary-chevron.
mv-unclaimed-markerMarker inserted in each item, data-kind nobody | asked | quiet | taken: .mv-unclaimed-seat, -marker-text, -marker-age.
mv-unclaimed-panelPopover panel (role=dialog): -head, -title, -sub, -sort / -sort-option, -rows, -row (data-tier nobody | quiet), -row-title, -row-meta, -impact, -row-actions, -drawer (-cands / -cand, -reasons / -reason, -note), -section (quiet tier), -empty.
mv-unclaimed-list / mv-unclaimed-itemList rendered by the items property: -item-main, -item-title, -item-context, -item-owner, .mv-unclaimed-avatar.
[data-unclaimed] / [data-unclaimed-age]Set on every item: unclaimed | stale | owned | dismissed, and fresh | settling | lingering | long for the first two, with --mv-unclaimed-age (0-1) for your own styling.

CSS variables

NameDefaultDescription
--mv-unclaimed-tonevar(--mv-warning)Hue of unclaimed markers, seats and chip; mixed in at low strength that grows with age.
--mv-unclaimed-quietvar(--mv-fg-subtle)Icon color of the “Quiet” marker.
--mv-unclaimed-takenvar(--mv-success)Color of the “Taken by …” confirmation and the clear state.
--mv-unclaimed-offset0pxSticky offset of the summary bar (below a fixed header).
--mv-unclaimed-age0-1 (set per item)Age ratio written on every item, marker and panel row (unclaimed: age / mature; stale: overdue quiet time).

Accessibility

Markers are real text inside each item (“Nobody yet”, “Asked Maya”, “Quiet”) with a visually hidden continuation (“, open for 18 days”, “, no update from Amara Okafor in 2 weeks”), so the state is never carried by color, dashes or tint alone; the short age (“18d”) is aria-hidden to avoid double reading. The summary is a native <button> whose accessible name is its visible text including the count (“5 follow-ups nobody owns, oldest 18 days, 2 gone quiet”), with aria-expanded and aria-controls; it opens a non-modal Popover API panel (role=dialog, labelled by its title) placed right after it in the DOM, so Tab walks straight into it; Escape or an outside click closes it and focus returns to the chip. The panel lists items in real <ul> lists; each row’s title is a button that scrolls to and focuses the item in the page (a short outline marks it), and every action is a button described by the row title (“I’ll take it, Add alerting on the webhook retry backlog”). Suggest someone and Not needed are disclosure buttons (aria-expanded, aria-controls) opening an inline drawer: candidates are buttons that read the name and the reason, dismissal reasons are a native radio group in a fieldset with a legend, the note has an accessible name, and Escape folds the drawer before closing the panel. The sort toggle is a labelled group of aria-pressed buttons. While a save is pending the button reads “Saving…” with aria-busy and aria-disabled; failures are announced assertively and shown under the row. Claims, takeovers, suggestions, check-ins and dismissals are announced politely with the remaining count (“You took “Rotate the staging API keys”. 4 follow-ups left nobody owns.”), and so are claims detected from elsewhere (“Lucas Moreau took …”). After a row leaves, focus moves to the next row’s first action. Reduced motion (OS or data-motion="reduce"): rows and panel appear and leave without sliding, the loading dots and the reveal outline do not animate. Forced colors: dashed seats and markers keep system borders, pressed and selected states use Highlight.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden