Exclusivebeta
Unclaimed — <mv-unclaimed>
Ownership radar for shared work lists: wrap any list, table or board of things everybody agrees matter (meeting action items, incident follow-ups, retro actions, bug triage, shared-inbox threads, alerts without an on-call, documentation debt) and every item whose data-owner is empty gets a calm “Nobody yet” marker, an empty dashed seat where an owner would sit, that becomes gently more present as it ages (tint, border and text firm up over the mature window; never red, never animated). A summary chip pinned to the list reads “5 things nobody owns · oldest 18 days · 2 gone quiet” and opens a panel sorted by age or impact where anyone can take an item in one click (cancelable mv-claim with waitUntil, the app persists), suggest someone from candidates the app supplies with the reason shown (“Maya Chen — Owns payments · 3 open tasks”; the item stays open, marked “Asked Maya”, until they accept) or archive it as not needed with a reason and a note. Claimed items whose owner has not touched them for the stale window form a softer second tier with Take over, Check in and, for your own, Still on it. Works on existing markup (data-owner, data-since, data-touched, data-impact) or renders from an items array; claims arriving from elsewhere (realtime, another device) are detected, flashed and announced.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-unclaimed>) |
| Status | beta |
| Also installs | button |
| Keywords | exclusive, 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
Install
node scripts/add.mjs unclaimed --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["unclaimed"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): 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.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<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 on Safari 18", 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>Cultural reference
Belling the Cat — Aesop (c. 600 BC, fable). The mice all agree that someone should hang a bell on the cat, but when asked who will do it, nobody volunteers, and the good idea stays undone. In the UI, work everyone agrees matters but nobody owns is made visible where it lives, with a calm marker that grows with age, a count pinned to the list and a one-click way to volunteer, name someone or drop it.
API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
item-selector | CSS 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. |
me | string | You | Name 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. |
stale | time ("7d", "36h", "2w"; bare number = days; "off") | 7d | A 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. |
mature | time | 14d | Age 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. |
sort | age | impact | age | Order of the panel: oldest first, or highest impact first (then oldest). Reflects the toggle in the panel header. |
noun / noun-plural | string | thing / things | What 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”. |
placement | top | bottom | top | Where the sticky summary bar sits when no [data-unclaimed-summary] host is present inside the element. |
reasons | comma-separated list | Already done, Duplicate, Out of scope, Won’t do | Reasons offered by “Not needed” (radio chips, plus an optional free-text note). |
now | ISO date | epoch ms | Reference 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" | true | false keeps the summary and panel but inserts no marker in the items (style them yourself from data-unclaimed and --mv-unclaimed-age). |
data-state | open | quiet | clear | Set by the component on itself and on the chip: some items have no owner, only quiet ones remain, or everything is owned. |
Properties
| Name | Type | Description |
|---|---|---|
items | Array<{ id, title, owner?, since?, touched?, impact?, context?, href?, suggested?, dismissed? }> | null | Render 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. |
candidates | Array<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. |
strings | Partial<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") => string | Localize 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). |
expanded | boolean | Whether the panel is open (read-only). |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-claim | Cancelable, 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-suggest | Cancelable, 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-dismiss | Cancelable, before an item is archived as not needed. detail: { item, reason, note, waitUntil(promise) }. |
mv-change | After 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-error | A waitUntil promise rejected; nothing changed. detail: { action, item, error }. |
mv-open / mv-close | The panel opened or closed. |
Content structure
| Name | Description |
|---|---|
[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
| Name | Description |
|---|---|
mv-unclaimed-bar / mv-unclaimed-summary | Sticky 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-marker | Marker inserted in each item, data-kind nobody | asked | quiet | taken: .mv-unclaimed-seat, -marker-text, -marker-age. |
mv-unclaimed-panel | Popover 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-item | List 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
| Name | Default | Description |
|---|---|---|
--mv-unclaimed-tone | var(--mv-warning) | Hue of unclaimed markers, seats and chip; mixed in at low strength that grows with age. |
--mv-unclaimed-quiet | var(--mv-fg-subtle) | Icon color of the “Quiet” marker. |
--mv-unclaimed-taken | var(--mv-success) | Color of the “Taken by …” confirmation and the clear state. |
--mv-unclaimed-offset | 0px | Sticky offset of the summary bar (below a fixed header). |
--mv-unclaimed-age | 0–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.