Exclusivobeta
Lead Board <mv-lead-board>
Um registro de investigação em que o único progresso é o que você sabe: pesquisa, entrevistas com usuários, investigações de incidentes, due diligence, caça a bugs, jornalismo. Cada item é uma descoberta em um de três estados (boato: ouvido falar, não verificado; explorado: verificado, com suas evidências; ou beco sem saída) ou uma pergunta, aberta ou respondida, e as descobertas se ligam umas às outras com “points to”, “contradicts” e “answers”. Duas visualizações do mesmo registro: um Outline (a fonte de verdade acessível: primeiro as perguntas, depois uma seção por grupo, uma pista aninhada sob a pista que aponta para ela, cada relação expressa em palavras como “Points to: Billing retries”, com a fonte e quem a registrou) e um Map (cards em faixas por grupo com setas desenhadas, dispostos automaticamente para que o mapa inteiro caiba no seu viewport, arrastáveis, com deslocamento e zoom por ponteiro, pinça, ctrl + roda ou teclado). “What’s unexplored?” classifica os boatos para os quais mais pistas vivas apontam, com o motivo (“Pointed to by 3 leads, 1 of them explored”), e os destaca nas duas visualizações. Responder a uma pergunta deixa de lado o ramo que só levava a ela: cada pista anterior a ela que não leve também a uma pergunta aberta é esmaecida e rotulada. Um painel de detalhe mostra as evidências, a procedência e as relações da pista selecionada como links de salto, e permite que a equipe mude seu estado, responda a uma pergunta e adicione ou remova ligações; um editor registra novas pistas. Os estados sempre vêm com um glifo distinto e uma palavra (anel tracejado, disco com marca, anel cortado, losango), nunca só a cor. Os dados vêm da marcação (itens [data-lead] com data-state, data-cluster, data-points-to…) ou da propriedade leads; mv-lead-state, mv-lead-add e mv-lead-link são canceláveis para que o app possa salvar ou vetar cada mudança.
| Categoria | Exibição de dados |
|---|---|
| Tipo | Web Component (<mv-lead-board>) |
| Status | beta |
| Também instala | button, input, select |
| Keywords | exclusive, culture, investigation, research, evidence, findings, rumor, leads, hypotheses, incident, post-mortem, bug-hunt, user-research, due-diligence, journalism, knowledge-graph, graph, outline, map, pan-zoom, drag |
When to use
- An incident or bug hunt collects unverified reports and verified facts, and the team must see which leads still need checking
- A research or discovery team logs interview findings and hunches and wants to know which hunch the evidence points to most
- Due diligence, audits or investigative reporting must keep every claim's source and author and show what contradicts what
- A long investigation spans days and people, and answered questions should stop drawing attention to the leads behind them
Avoid when
- The items are tasks moving through workflow stages rather than findings whose truth is being established → use Kanban instead
- Several sources report the same timestamped events and the job is reconciling who says what happened when → use Accounts instead
- Text claims must each show whether a citation backs them, inline in the prose → use Unsourced instead
Instalação
node scripts/add.mjs lead-board --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["lead-board"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, components/lead-board/lead-board.js, components/lead-board/lead-board.css, components/button/button.css, components/input/input.css, components/select/select.css.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div id="lb-demo" style="width:min(100%,78rem);margin-inline:auto">
<style>
#lb-demo { display:grid; gap:2.5rem }
#lb-demo .lb-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.75rem 1.5rem; flex-wrap:wrap; margin:0 0 .875rem }
#lb-demo .lb-kicker { display:flex; align-items:center; gap:.5rem; margin:0 0 .375rem; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
#lb-demo .lb-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#lb-demo .lb-sub { margin:.25rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem; max-width:64ch }
#lb-demo .lb-side { display:flex; align-items:center; gap:.75rem 1rem; flex-wrap:wrap }
#lb-demo .lb-side .mv-choice { font-size:.8125rem }
#lb-demo .lb-log { margin:.625rem 0 0; min-height:1.25rem; color:var(--mv-fg-muted); font-size:.75rem }
#lb-demo .lb-log b { color:var(--mv-fg); font-weight:600 }
#lb-demo #lb-incident { --mv-lead-board-map-height: 38rem }
</style>
<!-- Incident investigation, map view, declared in markup -->
<section aria-labelledby="lb-t1">
<div class="lb-head">
<div>
<p class="lb-kicker"><span class="mv-badge" data-variant="danger" data-shape="pill">SEV-2</span> INC-3107 · Opened Sep 19, 2026 · Day 5</p>
<h3 id="lb-t1">Some customers are charged twice at checkout</h3>
<p class="lb-sub">Dashed cards are rumors nobody has checked yet. Select a card to light up what it points to, drag cards around, or turn on What’s unexplored? to see which rumor the evidence converges on.</p>
</div>
<div class="lb-side">
<label class="mv-choice" data-control="end">
<input type="checkbox" role="switch" class="mv-switch" id="lb-readonly">
<span class="mv-choice-text"><span class="mv-choice-title">Read-only</span></span>
</label>
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="lb-teammate">Simulate a teammate’s lead</button>
</div>
</div>
<mv-lead-board id="lb-incident" view="map" author="Maya Okafor" label="INC-3107 investigation log">
<ul>
<li data-lead="why" data-kind="question" data-by="Amara Nwosu" data-at="2026-09-19" data-note="Top priority for the incident review on Sep 25.">Why are some customers charged twice?</li>
<li data-lead="who" data-kind="question" data-by="Amara Nwosu" data-at="2026-09-19">Which customers are affected?</li>
<li data-lead="release" data-state="answered" data-by="Kenji Watanabe" data-at="2026-09-19">Did the Sep 16 release cause it?</li>
<li data-lead="tickets" data-state="explored" data-cluster="Customer reports" data-source="Help desk view: Double charge" data-by="Amara Nwosu" data-at="2026-09-19" data-points-to="double-tap, who">42 tickets report a double charge since Sep 17</li>
<li data-lead="eu-cards" data-state="rumor" data-cluster="Customer reports" data-source="Support standup, Sep 19" data-by="Amara Nwosu" data-at="2026-09-19" data-points-to="three-ds, who">Agents say it is mostly EU cards</li>
<li data-lead="double-tap" data-state="rumor" data-cluster="Customer reports" data-source="App Store review, Sep 18" data-by="Leo Martins" data-at="2026-09-20">Pay button can be tapped twice on slow networks</li>
<li data-lead="timeouts" data-state="explored" data-cluster="Billing service" data-source="Latency dashboard, provider calls p99" data-by="Kenji Watanabe" data-at="2026-09-20" data-points-to="retries">Provider calls time out at 10 s since Sep 17</li>
<li data-lead="retries" data-state="explored" data-cluster="Billing service" data-source="billing-api retry policy, charge.ts" data-by="Kenji Watanabe" data-at="2026-09-20" data-points-to="idempotency">Billing retries a charge after a timeout</li>
<li data-lead="web-share" data-state="explored" data-cluster="Billing service" data-source="Payments export, Sep 17-21" data-by="Leo Martins" data-at="2026-09-21" data-contradicts="double-tap">17 of 42 double charges came from web checkout</li>
<li data-lead="no-key" data-state="rumor" data-cluster="Billing service" data-source="Overheard in #billing" data-by="Sofia Marino" data-at="2026-09-21" data-points-to="idempotency">Retries may go out without an idempotency key</li>
<li data-lead="three-ds" data-state="rumor" data-cluster="Payment provider" data-source="Provider support email, Sep 20" data-by="Sofia Marino" data-at="2026-09-20" data-points-to="idempotency">3-D Secure retries fail silently on some EU banks</li>
<li data-lead="idempotency" data-state="rumor" data-cluster="Payment provider" data-source="Provider community forum thread" data-by="Sofia Marino" data-at="2026-09-21" data-answers="why">Provider ignores idempotency keys on 3-D Secure retries</li>
<li data-lead="sales-said" data-state="dead-end" data-cluster="Release" data-source="#sales thread, Sep 18" data-by="Amara Nwosu" data-at="2026-09-18" data-points-to="release">Sales says the new checkout shipped Sep 16</li>
<li data-lead="diff" data-state="explored" data-cluster="Release" data-source="Release diff v4.18.0 to v4.19.0" data-by="Kenji Watanabe" data-at="2026-09-19" data-contradicts="sales-said" data-answers="release">The Sep 16 release has no billing changes</li>
</ul>
</mv-lead-board>
<p class="lb-log" id="lb-log1" aria-live="off">Keyboard: arrow keys move between cards, Shift + arrows move one, + and − zoom, 0 fits.</p>
</section>
<!-- User research, outline view, data from the leads property -->
<section aria-labelledby="lb-t2">
<div class="lb-head">
<div>
<p class="lb-kicker"><span class="mv-badge" data-variant="secondary" data-shape="pill">Discovery</span> Trial activation study · Sep 8-22, 2026</p>
<h3 id="lb-t2">Why do trial teams stall in week 2?</h3>
<p class="lb-sub">Six interviews, two dashboards and a pricing experiment. The pricing question is answered, so the leads behind it step back; What’s unexplored? ranks the hunches the evidence keeps pointing to.</p>
</div>
<div class="lb-side">
<button class="mv-button" data-variant="outline" data-size="sm" type="button" id="lb-answer">Mark the top lead explored</button>
</div>
</div>
<mv-lead-board id="lb-research" unexplored selected="a-import" author="Hana Kobayashi" label="Trial activation research log"></mv-lead-board>
<p class="lb-log" id="lb-log2" aria-live="off"></p>
</section>
<script type="module">
const incident = document.getElementById("lb-incident");
const research = document.getElementById("lb-research");
const log1 = document.getElementById("lb-log1");
const log2 = document.getElementById("lb-log2");
const say = (el, [label, text = ""]) => { const b = document.createElement("b"); b.textContent = label; el.replaceChildren(b, text); };
research.leads = [
{ id: "q-stall", kind: "question", title: "Why do trial teams stall in week 2?", by: "Hana Kobayashi", at: "2026-09-08" },
{ id: "q-price", kind: "question", state: "answered", title: "Is pricing a blocker for small teams?", by: "Chloe Martin", at: "2026-09-08" },
{ id: "i-invite", state: "explored", cluster: "Interviews", title: "4 of 6 admins never invited a second teammate", source: "Interview notes, sessions 1-6", by: "Hana Kobayashi", at: "2026-09-12", pointsTo: ["a-solo", "q-stall"] },
{ id: "i-import", state: "rumor", cluster: "Interviews", title: "Admins say the CSV import “broke” their data", source: "Session 3, operations lead", by: "Hana Kobayashi", at: "2026-09-10", pointsTo: ["a-import"] },
{ id: "i-champion", state: "rumor", cluster: "Interviews", title: "Trials stall when the champion goes on vacation", source: "Session 5, aside after the call", by: "Hana Kobayashi", at: "2026-09-15", pointsTo: ["q-stall"] },
{ id: "a-solo", state: "explored", cluster: "Product analytics", title: "Single-seat workspaces churn 3× more in week 2", source: "Retention dashboard, August cohort", by: "Omar Haddad", at: "2026-09-16", pointsTo: ["a-template"] },
{ id: "a-template", state: "rumor", cluster: "Product analytics", title: "Teams that skip templates never build a second board", source: "Hunch from the August funnel review", by: "Omar Haddad", at: "2026-09-16", pointsTo: ["q-stall"] },
{ id: "a-support", state: "explored", cluster: "Product analytics", title: "Support logged 23 import tickets in September", source: "Help desk tag: import", sourceHref: "#", by: "Omar Haddad", at: "2026-09-18", pointsTo: ["a-import"] },
{ id: "a-import", state: "rumor", cluster: "Product analytics", title: "Imports over 5,000 rows time out without an error", source: "Support team, not reproduced yet", by: "Omar Haddad", at: "2026-09-18", note: "If true, the team’s first real data never lands and the trial looks empty in week 2.", pointsTo: ["q-stall"] },
{ id: "s-import", state: "rumor", cluster: "Sales calls", title: "Two prospects asked whether import takes Excel files", source: "Sales call notes, Sep 11", by: "Chloe Martin", at: "2026-09-11", pointsTo: ["a-import"] },
{ id: "s-price", state: "dead-end", cluster: "Sales calls", title: "Small teams say the per-seat price is too high", source: "Recorded sales calls, August", by: "Chloe Martin", at: "2026-09-09", pointsTo: ["q-price"] },
{ id: "s-discount", state: "explored", cluster: "Sales calls", title: "Trials with a 50% discount stall at the same rate", source: "Pricing experiment #12", by: "Chloe Martin", at: "2026-09-19", contradicts: ["s-price"], answers: ["q-price"] },
];
// Teammate activity: a new explored finding lands on the incident map.
let added = false;
document.getElementById("lb-teammate").addEventListener("click", (e) => {
if (added) return;
const id = incident.addLead({
id: "captures", state: "explored", cluster: "Payment provider", by: "Kenji Watanabe",
title: "Provider shows two captures for one authorization", source: "Provider ticket #55120",
links: [{ to: "idempotency", type: "points-to" }],
}, { select: false });
if (id) { added = true; e.currentTarget.disabled = true; }
});
document.getElementById("lb-readonly").addEventListener("change", (e) => { incident.readonly = e.target.checked; });
incident.addEventListener("mv-lead-add", (e) => say(log1, ["Logged: ", `${e.detail.lead.title} (${e.detail.lead.by})`]));
incident.addEventListener("mv-lead-state", (e) => say(log1, ["State: ", `${e.detail.lead.title} → ${e.detail.state}`]));
incident.addEventListener("mv-lead-link", (e) => say(log1, [`Link ${e.detail.action === "add" ? "added" : "removed"}: `, `${e.detail.from} ${e.detail.type} ${e.detail.to}`]));
incident.addEventListener("mv-lead-move", (e) => say(log1, ["Moved: ", `${e.detail.id} to ${e.detail.x}, ${e.detail.y}`]));
research.addEventListener("mv-lead-state", (e) => say(log2, ["State: ", `${e.detail.lead.title} → ${e.detail.state}`]));
research.addEventListener("mv-lead-select", (e) => say(log2, ["Selected: ", e.detail.lead.title]));
document.getElementById("lb-answer").addEventListener("click", () => {
const top = research.promising[0];
if (top) research.setState(top.id, "explored");
else say(log2, ["Nothing left: ", "every rumor has been checked or set aside."]);
});
</script>
</div>Referência cultural
Outer Wilds, Mobius Digital (2019, jogo). O único progresso é o conhecimento: o diário de bordo da nave registra boatos e descobertas como cards ligados, marcando quais pistas só foram ouvidas e quais foram exploradas, com setas que mostram o que aponta para o quê. Na interface, isso vira um registro de investigação cujo outline e cujo mapa mostram as descobertas como boatos, exploradas ou becos sem saída com suas ligações, classificam os boatos inexplorados para os quais mais evidências apontam e deixam um ramo de lado quando sua pergunta é respondida.
API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
view | outline | map | outline | Which view is shown. Both render the same log; the outline is the accessible source of truth and the map mirrors it. The built-in Outline / Map switch sets it. |
selected | string | Id of the selected lead: highlighted in both views (its links and neighbors lit on the map, the rest faded) and shown in the detail panel. On the map, the detail column only opens while a lead is selected. | |
unexplored | boolean | “What’s unexplored?” mode: a Most promising leads strip ranks unexplored rumors (not set aside) by how many live leads point to them (dead ends don’t count; ties go to pointers that are explored, then to rumors leading to an open question); rumors get rank badges, arrows into rumors are lit, everything else steps back. The toolbar toggle sets it. | |
readonly | boolean | Hides the composer, state controls, link editor and unlink buttons. Selecting, reading and arranging the map still work. | |
author | string | Name recorded as “Logged by” on leads added from the composer or addLead() (when the lead doesn’t set by). | |
locale | string | en-US | Locale for dates (“Sep 19”, with the year when it isn’t the current one). Date-only values (2026-09-19) are read as calendar dates, never shifted by time zone. |
label | string | Investigation log | Accessible name of the region. |
heading-level | number | 3 | Heading level (2-6) of the outline sections and of the detail title. |
data-view / data-unexplored / data-readonly / data-has-selection | set by the component | Reflect the current view and modes on the host, for styling. |
Properties
| Name | Tipo | Description |
|---|---|---|
leads | Array<{ id?, title, kind?: "finding" | "question", state?: "rumor" | "explored" | "dead-end" | "open" | "answered", cluster?, note?, source?, sourceHref?, by?, at?, links?: Array<{ to, type: "points-to" | "contradicts" | "answers" }>, pointsTo?, contradicts?, answers?, x?, y? }> | The log as data (takes precedence over markup). pointsTo / contradicts / answers accept an array or a comma-separated list of ids as shorthand for links. state "question", "open" or "answered" implies kind question. x / y pin a card on the map. Reading it returns the current log, including edits and manual map positions (as x / y). |
summary | { rumors, explored, deadEnds, questions, answered, setAside } | Counts (read-only). setAside is the number of leads dimmed by answered questions. |
promising | Array<{ id, title, rank, score, verified, reason }> | Unexplored rumors in ranked order (read-only): score = live leads pointing to it, verified = how many of them are explored, reason = the sentence shown in the UI. |
strings | Partial<Record<string, string>> | Overrides for every visible text and announcement (English defaults), including state and relation names (rumor, explored, dead-end, open, answered, points-to, pointed-by, contradicts, contradicted-by, answers, answered-by, proposed-by), plurals written "one|many" with {n}. |
Methods
| Name | Description |
|---|---|
select(id, { focus? }) | Selects a lead (null clears) and optionally focuses it in the current view. Emits mv-lead-select. Returns false for an unknown id. |
setState(id, state) | Changes a finding’s state (rumor | explored | dead-end) or a question’s (open | answered). Emits the cancelable mv-lead-state; returns false if vetoed or invalid. |
answer(questionId, { by? }) | Marks a question answered, first linking the finding given as by with “answers” if needed. Its resolved branch is set aside. |
addLead(lead, { select? }) | Adds a lead (same shape as leads items; by defaults to author, at to today). Emits the cancelable mv-lead-add; returns the new id or null. |
link(from, to, type) / unlink(from, to, type) | Adds or removes a relation (points-to | contradicts | answers; answers must target a question). Emits the cancelable mv-lead-link; returns false if vetoed or invalid. |
arrange() | Re-runs the automatic map layout (manual positions are dropped) and fits the map; cards glide to their new place. |
fit() / zoom(factor, { to? }) | Fits the whole map in its viewport, or zooms around the viewport center (scale 0.3 to 2). |
Events
| Name | Description |
|---|---|
mv-lead-select | A lead was selected. detail: { id, lead, view }. |
mv-lead-state | Cancelable, before a state change. detail: { id, state, previous, lead }. preventDefault() keeps the previous state. |
mv-lead-add | Cancelable, before a lead is added. detail: { lead, reason: "form" | "api" }. detail.lead may be modified (e.g. a server id) before it is added; preventDefault() drops it. |
mv-lead-link | Cancelable, before a relation is added or removed. detail: { from, to, type, action: "add" | "remove" }. |
mv-lead-move | A card was dragged or moved with Shift + arrow keys on the map. detail: { id, x, y } (world pixels, snapped to an 8 px grid). Persist it and pass it back as x / y. |
CSS classes
| Name | Description |
|---|---|
mv-lead-board-bar / -tally / -promising / -composer | Toolbar (views, What’s unexplored?, Log a lead), counting legend, ranked strip, and the new-lead form. |
mv-lead-board-outline / -group / -list / -node / -row / -item / -desc | Outline: one section per group, nested lists, a row per lead whose button (-item) holds the state tag and title and whose -desc spells out rank, relations (-rel with data-rel), source and author. -node carries data-state, data-kind, data-resolved, data-rank, data-selected. |
mv-lead-board-map / -viewport / -world / -band / -edge / -edge-label / -card / -map-tools / -legend | Map: pannable viewport, transformed world, cluster bands, SVG arrows (data-type, data-hot, data-cold, data-resolved), cards (data-state, data-kind, data-rank, aria-current, data-lifted while dragged), zoom tools and the arrow legend. |
mv-lead-board-detail | Detail panel (data-state, data-kind): kicker, title, note, -facts (evidence, logged by), -detail-rel groups of jump links, -states radiogroup, -linker. |
mv-lead-board-tag / -glyph / -rank | State pill (glyph + word, dashed for rumors), the state glyphs (.g-fill, .g-ink, .g-line, .g-dash) and the rank badge (data-top for #1). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-lead-board-rumor | var(--mv-warning) | Tone of rumors, of the What’s unexplored? mode and of rank badges. |
--mv-lead-board-explored | var(--mv-success) | Tone of explored findings. |
--mv-lead-board-dead-end | var(--mv-fg-subtle) | Tone of dead ends. |
--mv-lead-board-question | var(--mv-accent) | Tone of open questions and of “answers” arrows. |
--mv-lead-board-answered | var(--mv-success) | Tone of answered questions and of the set-aside note. |
--mv-lead-board-contradicts | var(--mv-danger) | Tone of “contradicts” arrows (also dashed and labeled). |
--mv-lead-board-map-height | 34rem | Height of the map view. |
--mv-lead-board-detail-width | 20rem | Width of the detail column (it moves under the view below a 52rem-wide container). |
Accessibility
The outline is the primary view and the source of truth; the map mirrors it and never holds information the outline lacks. The component is a labeled region. Outline sections have real headings (heading-level); each lead is a <button> in a nested list, reached with a single Tab stop (roving tabindex: Up / Down, Home / End, typeahead on titles), whose name starts with its state in words (“Rumor: Provider ignores idempotency keys…”) and whose aria-describedby text spells out its rank in unexplored mode, every relation (“Points to: Billing retries. Contradicted by: 17 of 42 double charges came from web checkout”), its source (“Heard from” for rumors, “Evidence” for findings) and who logged it when, and whether an answered question set it aside. The selected lead has aria-current. Map cards are buttons with the same name and the same description (they reference the outline text); one Tab stop, arrow keys move spatially to the nearest card in that direction, Shift + arrow keys move the focused card (the keyboard alternative to dragging, reported by mv-lead-move), + / − zoom, 0 fits, and the viewport itself is focusable so arrow keys pan it; focus is always scrolled into view. Arrows, band labels and edge labels are aria-hidden because relations are already in text. The Outline / Map switch is a radiogroup, What’s unexplored? a toggle button (aria-pressed), Log a lead a disclosure (aria-expanded) opening a labeled form; the state control in the detail panel is a radiogroup (arrow keys change the state), relations there are jump links that select and focus the related lead, unlink buttons have explicit names. Escape clears the selection and returns focus to the lead, or closes the composer. View changes, state changes, answered questions (with how many leads were set aside), new leads, links and the top unexplored lead are announced in a polite live region. States never rely on color: each has its own glyph (dashed ring, check disc, slashed ring, diamond, check diamond) and word, rumors have dashed borders, dead ends are struck through and labeled, set-aside leads carry text as well as dimming. Forced colors keep arrows, glyphs and the selection visible (CanvasText, Highlight). Reduced motion (OS or data-motion="reduce"): cards jump to a new layout instead of gliding and the camera moves without easing.