---
title: Contributions
---

```js
import {API, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
const POLL = 30_000;
```

```js
// Live poll for contributions
const contribState = (async function*() {
  let failures = 0;
  while (true) {
    let data = [], ok = false;
    try {
      const r = await apiFetch("/contributions/");
      ok = r.ok;
      data = ok ? await r.json() : [];
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {data, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL, failures}));
  }
})();
```

```js
const contribs = contribState.data ?? [];
const _ok = contribState.ok ?? false;
const _ts = contribState.ts;
```

# Contributions

```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp}  from "./components/doc-overlay.js";

const _liveEl = html`<div class="live-indicator">
  <span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
  ${_ok ? `Live · ${_ts?.toLocaleTimeString()}` : html`<span style="color:red">API offline</span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/contributions"); }
```

```js
// Filters
const typeFilter  = Inputs.select(["all", "br", "fr", "ep", "upr"], {label: "Type", value: "all"});
const statFilter  = Inputs.select(
  ["all", "draft", "submitted", "acknowledged", "accepted", "rejected", "merged", "withdrawn"],
  {label: "Status", value: "all"}
);
const repoFilter  = Inputs.text({label: "Target repo", placeholder: "filter by repo…"});
display(html`<div style="display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem">
  ${typeFilter}${statFilter}${repoFilter}
</div>`);
```

```js
const tf = typeFilter.value;
const sf = statFilter.value;
const rf = repoFilter.value?.trim().toLowerCase() ?? "";

const filtered = contribs.filter(c =>
  (tf === "all" || c.type === tf) &&
  (sf === "all" || c.status === sf) &&
  (!rf || (c.target_repo ?? "").toLowerCase().includes(rf))
);
```

## Summary

```js
const typeLabels = {br: "Bug Report", fr: "Feature Request", ep: "Extension Point", upr: "Upstream PR"};
const typeCounts = Object.fromEntries(["br","fr","ep","upr"].map(t => [
  t, contribs.filter(c => c.type === t).length
]));
const needsFollowUp = contribs.filter(c => ["submitted","acknowledged"].includes(c.status)).length;

display(html`<div class="grid grid-cols-5" style="gap:1rem;margin-bottom:1.5rem">
  <div class="card">
    <h3>Total</h3>
    <p class="big-num">${contribs.length}</p>
  </div>
  ${["br","fr","ep","upr"].map(t => html`
    <div class="card">
      <h3>${typeLabels[t]}</h3>
      <p class="big-num">${typeCounts[t]}</p>
    </div>
  `)}
</div>
${needsFollowUp > 0 ? html`<div class="follow-up-banner">⚠ ${needsFollowUp} contribution(s) awaiting upstream response (submitted / acknowledged)</div>` : ""}
`);
```

## Status Kanban

```js
const statusCols = [
  {key: "draft",        label: "Draft",       color: "#aaa"},
  {key: "submitted",    label: "Submitted",   color: "steelblue"},
  {key: "acknowledged", label: "Acknowledged",color: "#f0a500"},
  {key: "accepted",     label: "Accepted",    color: "#4caf50"},
  {key: "merged",       label: "Merged",      color: "#2e7d32"},
  {key: "rejected",     label: "Rejected",    color: "#e53935"},
  {key: "withdrawn",    label: "Withdrawn",   color: "#bbb"},
];

const colMap = {};
for (const c of filtered) {
  (colMap[c.status] = colMap[c.status] ?? []).push(c);
}

const activeCols = statusCols.filter(s => colMap[s.key]?.length);
if (activeCols.length === 0) {
  display(html`<p style="color:gray">No contributions match the current filters.</p>`);
} else {
  display(html`<div class="kanban">
    ${activeCols.map(s => html`
      <div class="kanban-col">
        <div class="kanban-header" style="border-bottom:2px solid ${s.color}">${s.label} <span class="kanban-count">${colMap[s.key].length}</span></div>
        ${colMap[s.key].map(c => html`
          <div class="contrib-card">
            <div class="contrib-badge contrib-badge-${c.type}">${c.type.toUpperCase()}</div>
            <div class="contrib-title">${c.title}</div>
            ${c.target_org || c.target_repo ? html`<div class="contrib-repo">${[c.target_org, c.target_repo].filter(Boolean).join("/")}</div>` : ""}
            ${c.body_path ? html`<div class="contrib-path">${c.body_path}</div>` : ""}
            <div class="contrib-date">${new Date(c.created_at).toLocaleDateString()}</div>
          </div>
        `)}
      </div>
    `)}
  </div>`);
}
```

## All Contributions

```js
display(Inputs.table(filtered.map(c => ({
  Type:    c.type.toUpperCase(),
  Title:   c.title,
  Status:  c.status,
  Target:  [c.target_org, c.target_repo].filter(Boolean).join("/") || "—",
  Created: new Date(c.created_at).toLocaleDateString(),
})), {maxWidth: 900}));
```

<style>
.live-indicator { font-size: 0.8rem; color: gray; padding: 0.55rem 0.7rem; margin-bottom: 0.75rem; }
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
.big-num { font-size: 2.2rem; font-weight: bold; margin: 0.25rem 0; }
.follow-up-banner { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 0.5rem 0.9rem; margin-bottom: 1rem; font-size: 0.9rem; }
.kanban { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.kanban-col { background: var(--theme-background-alt); border-radius: 8px; padding: 0.75rem; }
.kanban-header { font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; padding-bottom: 0.5rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.4rem; }
.kanban-count { font-size: 0.75rem; background: var(--theme-background); border-radius: 10px; padding: 0.1rem 0.4rem; font-weight: 500; }
.contrib-card { background: var(--theme-background); border-radius: 6px; padding: 0.6rem 0.75rem; margin-bottom: 0.5rem; }
.contrib-badge { display: inline-block; font-size: 0.65rem; font-weight: 700; border-radius: 3px; padding: 0.1rem 0.35rem; margin-bottom: 0.25rem; }
.contrib-badge-br  { background: #fde8e8; color: #c62828; }
.contrib-badge-fr  { background: #e3f2fd; color: #1565c0; }
.contrib-badge-ep  { background: #f3e5f5; color: #6a1b9a; }
.contrib-badge-upr { background: #e8f5e9; color: #2e7d32; }
.contrib-title { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.2rem; line-height: 1.3; }
.contrib-repo  { font-size: 0.75rem; color: steelblue; font-family: monospace; }
.contrib-path  { font-size: 0.7rem; color: gray; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contrib-date  { font-size: 0.7rem; color: gray; margin-top: 0.3rem; }
</style>
