独家

Unclaimed <mv-unclaimed>

共享工作清单的归属雷达:包裹任何大家都认为重要的事项列表、表格或看板(会议行动项、事故后续、回顾行动、bug 分诊、共享收件箱会话、无人值守的告警、文档欠账),每个 data-owner 为空的条目都会显示一个平静的“Nobody yet”标记,即负责人所在位置上的一个虚线空座位,它会随时间推移逐渐变得醒目(在成熟期内色调、边框和文字逐渐加深;从不变红,从不动画)。

文化出处

《给猫挂铃铛》,伊索(约公元前 600 年,寓言)。老鼠们都同意应该有人给猫挂上铃铛,但当被问到谁去做时,没有人自告奋勇,这个好主意也就一直没人去做。在界面中,大家都认为重要却无人负责的工作会在其所在之处显现出来:一个随时间推移而愈发醒目的平静标记、一个固定在列表上的计数,以及一键认领、指派他人或放弃的方式。

工作原理

共享工作列表的归属雷达:包裹任何列表、表格或看板,其中是大家都认为重要的事项(会议待办事项、事故后续跟进、回顾行动项、缺陷分诊、共享收件箱会话、无人值班的告警、文档债务),每个 data-owner 为空的项目都会得到一个平静的 “Nobody yet” 标记,即负责人本该在的位置上的一个虚线空座位,它会随时间推移逐渐变得更醒目(色调、边框和文字在 mature 窗口内逐渐加深;从不变红,也从不动画)。固定在列表上的汇总标签显示 “5 things nobody owns · oldest 18 days · 2 gone quiet”,点击后打开按时长或影响排序的面板,任何人都可以一键认领某项(可取消的 mv-claim,支持 waitUntil,由应用持久化)、从应用提供的候选人中推荐某人并显示理由(“Maya Chen · Owns payments · 3 open tasks”;该项保持开放,标记为 “Asked Maya”,直到对方接受),或附上原因和备注将其归档为不需要。已被认领但负责人在 stale 窗口内未处理的项目构成较柔和的第二层级,提供 Take over、Check in,以及针对你自己项目的 Still on it。可作用于现有标记(data-owner、data-since、data-touched、data-impact),或从 items 数组渲染;来自其他地方的认领(实时更新、另一台设备)会被检测到、闪烁提示并播报。

分类数据展示
类型Web Component(<mv-unclaimed>)
状态稳定版
同时安装button
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

安装

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

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

复制的文件(含依赖):tokens/tokens.css, core/base.css, components/button/button.css, core/dismiss.js, core/dom.js, core/element.js, core/motion.js, core/position.js, components/unclaimed/unclaimed.js, components/unclaimed/unclaimed.css。

用法

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

<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>

API

Attributes

Name类型DefaultDescription
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.
localeBCP 47 tagen-USLocale of the dates in the marker tooltips (“Open since Sep 6, 2026”). An unknown tag falls back to en-US.
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

Name类型Description
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, openSince, lastUpdate…). English defaults with {count}, {noun}, {age}, {title}, {name}, {owner}, {date} placeholders. Ages come from formatAge (English by default): set formatAge along with strings when you translate.
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. Known limits: ownership is read from the data-owner attributes (or the items property) the app renders, so a claim made elsewhere shows up only when the app updates them; ages are computed from the viewer's clock unless now is set.

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