익스클루시브

Unclaimed <mv-unclaimed>

공유 작업 목록을 위한 담당자 레이더입니다. 모두가 중요하다고 동의하는 일들의 목록, 테이블, 보드(회의 액션 아이템, 장애 후속 조치, 회고 액션, 버그 분류, 공유 받은편지함 스레드, 당직자 없는 알림, 문서 부채)를 감싸면 data-owner가 비어 있는 모든 항목에 차분한 “Nobody yet” 표시가 붙습니다. 담당자가 앉을 자리를 나타내는 빈 점선 좌석으로, 시간이 지날수록 서서히 더 두드러집니다(성숙 기간 동안 색조, 테두리, 텍스트가 진해지며, 빨간색이나 애니메이션은 절대 쓰지 않습니다).

문화적 레퍼런스

고양이 목에 방울 달기, 이솝 (기원전 600년경, 우화). 쥐들은 누군가 고양이 목에 방울을 달아야 한다는 데 모두 동의하지만, 누가 할지 묻자 아무도 나서지 않고 좋은 생각은 실행되지 않은 채로 남습니다. UI에서는 모두가 중요하다고 동의하지만 아무도 맡지 않은 일을 그 일이 있는 곳에서 바로 보이게 합니다. 시간이 지날수록 뚜렷해지는 차분한 표시, 목록에 고정된 개수, 그리고 한 번의 클릭으로 자원하거나, 누군가를 지명하거나, 없애는 방법을 제공합니다.

작동 방식

공유 작업 목록을 위한 소유권 레이더입니다. 모두가 중요하다고 동의하는 일들(회의 실행 항목, 인시던트 후속 조치, 회고 액션, 버그 분류, 공유 받은편지함 스레드, 당직자 없는 알림, 문서 부채)의 목록, 표, 보드를 감싸면, data-owner가 비어 있는 모든 항목에 차분한 “Nobody yet” 표시가 붙습니다. 소유자가 앉을 자리에 점선으로 된 빈자리가 생기고, 시간이 지날수록 조금씩 더 뚜렷해집니다(성숙 기간 동안 색조, 테두리, 텍스트가 진해지며, 빨간색이나 애니메이션은 절대 쓰지 않습니다). 목록에 고정된 요약 칩에는 “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로 번역되었습니다. 번역 문제 신고