# Contribution Graph Widget — Design Spec

**Date:** 2026-05-08
**Status:** Approved
**Scope:** Hugo partial + vanilla JS + CSS additions to theme repo

---

## Overview

A sidebar/footer widget that renders a 53×7 GitHub-style contribution graph fetched from `/contributions.json` on danix.xyz. Each cell is split left/right representing gitolite (green) and GitHub (purple) activity independently. Placed in two locations: sidebar on `/is` and `/it/is` pages only, and footer right column sitewide.

---

## Data Source

- **URL:** `https://danix.xyz/contributions.json`
- **Format:** Flat object keyed by ISO date strings `"YYYY-MM-DD"`, each with `{ "github": N, "gitolite": N }`
- **Coverage:** Always exactly the last 365 days, all dates present, zero counts included
- **Already served:** No generation needed, endpoint is live

---

## Placement

### 1. Sidebar — `/is` and `/it/is` only

- Injected in `layouts/is/list.html` after the stats widget (before social share)
- Cell size: 8px, gap: 2px
- Sidebar is `md:col-span-1` (~200–220px) — graph fits at 8px × 53 cols + gaps = ~530px total grid width; horizontal scroll allowed via `overflow-x: auto`

### 2. Footer — right column, sitewide

- Appended below existing `<dl>` terminal readout in `layouts/partials/footer.html`
- Cell size: 6px, gap: 1px
- Fits the narrower right column

### Caller Pattern

Both call sites use:
```hugo
{{- partial "contribution-graph.html" (dict "cellSize" 8) -}}
{{- partial "contribution-graph.html" (dict "cellSize" 6) -}}
```

---

## Cell Rendering

**Approach:** Single `div` per cell, `linear-gradient(90deg, green 50%, purple 50%)`.

- Left half = gitolite = `--accent2` (green: `#00ff88` dark / `#008f5a` light)
- Right half = github = `--accent` (purple: `#a855f7` dark / `#7c3aed` light)
- Empty cell background = `var(--border)`

**Intensity levels** (evaluated independently per source):

| Count | Opacity |
|-------|---------|
| 0 | transparent (`var(--border)` side) |
| 1–3 | 0.35 |
| 4–9 | 0.60 |
| 10–19 | 0.80 |
| 20+ | 1.00 |

If both sources are zero: solid `var(--border)` background (no gradient).
If one source is zero: that half uses `var(--border)` color, other half uses accent with opacity.

Opacity against `--accent`/`--accent2` automatically adapts to dark/light theme — no extra theme rules needed.

---

## Grid Structure

- 53 columns × 7 rows
- Sunday = row 0 (top), Saturday = row 6 (bottom)
- Newest date = rightmost column
- CSS: `grid-template-rows: repeat(7, var(--contrib-cell-size, 8px)); grid-auto-flow: column`
- Month labels row above grid: abbreviated month name (`Jan`, `Feb`, …) placed at the column index where that month first appears. Labels may be 1–2 columns off at year boundary — accepted cosmetic limitation.

---

## Tooltip

- Triggered on `mouseover` / `mouseout` per cell (mouse-only; cells are not tab-focusable)
- Content: date (`Mon DD, YYYY`), gitolite count, github count, total
- Implementation: single shared `div.contrib-tooltip` injected into `<body>`, positioned via `position: fixed` + JS mouse coordinates (avoids sidebar overflow clipping)
- `aria-hidden="true"` — decorative; screen readers use cell `aria-label` instead
- No CSS transition (instant show/hide — no `prefers-reduced-motion` concern)

---

## Summary Line

Below the grid:
```
N commits in the last year · G from gitolite · H from GitHub
```
Uses i18n key `contrib_summary` with Hugo-style params.

---

## Architecture

### Files Changed (all in `~/Programming/GIT/danix2-hugo-theme/`)

| File | Change |
|------|--------|
| `layouts/partials/contribution-graph.html` | New — container + script tag |
| `assets/js/contribution-graph.js` | New — fetch, build, render, tooltip |
| `assets/css/main.css` | Edit — add `.contrib-*` classes |
| `layouts/is/list.html` | Edit — call partial (cellSize 8) after stats widget |
| `layouts/partials/footer.html` | Edit — call partial (cellSize 6) at end of right column |
| `i18n/en.yaml` | Edit — add contrib keys |
| `i18n/it.yaml` | Edit — add contrib keys (Italian) |

Then in content repo: `npm run build` → commit compiled CSS → bump submodule pointer.

### JS Flow (`contribution-graph.js`)

1. Read `cellSize` from container dataset attribute
2. Fetch `https://danix.xyz/contributions.json`
3. On any fetch/parse error → hide container element, return
4. Sort dates oldest→newest; assign to 53-col × 7-row grid slots (Sun=0 … Sat=6)
5. Compute gitolite level and github level independently per date → derive gradient string
6. Build month label row: track first column index per calendar month
7. Build grid: one `div.contrib-cell` per date, set `background` inline, set `aria-label`
8. Inject tooltip `div` into `<body>` once (shared across all cells on page)
9. Attach `mouseover`/`mouseout` listeners on grid (event delegation) for tooltip positioning
10. Inject summary line below grid
11. Reveal container (was hidden via inline `display:none` until ready)

### HTML Structure (rendered by JS)

```html
<div id="contrib-graph" style="display:none" data-cell-size="8" data-lang="en">
  <figure role="img" aria-label="Contribution activity for the last year">
    <div class="contrib-graph-wrap">
      <div class="contrib-month-row">…month labels…</div>
      <div class="contrib-grid">
        <div class="contrib-cell" aria-label="May 8, 2026: 12 gitolite, 3 github"></div>
        …
      </div>
    </div>
    <figcaption class="contrib-summary">342 commits in the last year · 270 from gitolite · 72 from GitHub</figcaption>
  </figure>
</div>
<!-- injected once into body: -->
<div class="contrib-tooltip" aria-hidden="true"></div>
```

---

## CSS Classes

```css
.contrib-graph-wrap { overflow-x: auto; }

.contrib-month-row {
  display: grid;
  grid-auto-flow: column;
  gap: 2px;
  margin-bottom: 2px;
}
.contrib-month-label {
  font-size: 0.55rem;
  color: var(--text-dim);
  font-family: var(--font-mono);
}

.contrib-grid {
  display: grid;
  grid-template-rows: repeat(7, var(--contrib-cell-size, 8px));
  grid-auto-flow: column;
  gap: var(--contrib-gap, 2px);
  width: fit-content;
}

.contrib-cell {
  width: var(--contrib-cell-size, 8px);
  height: var(--contrib-cell-size, 8px);
  border-radius: 1px;
  background: var(--border);
  cursor: default;
}

.contrib-tooltip {
  position: fixed;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 5px 8px;
  font-size: 0.65rem;
  color: var(--text);
  font-family: var(--font-mono);
  pointer-events: none;
  z-index: 50;
  white-space: nowrap;
  display: none;
}
.contrib-tooltip.visible { display: block; }

.contrib-summary {
  font-size: 0.65rem;
  color: var(--text-dim);
  margin-top: 6px;
  font-family: var(--font-mono);
}
```

JS sets `--contrib-cell-size` and `--contrib-gap` as inline CSS vars on the grid element based on `cellSize` param.

---

## i18n Keys

Hugo renders only the widget heading and the `aria-label` at build time. The summary line is built by JS after fetch, so it uses a bundled JS translation map (not Hugo i18n).

### `i18n/en.yaml` and `i18n/it.yaml` (Hugo-rendered strings)
```yaml
# en.yaml
contrib_widget_label: "Contribution activity for the last year"
contrib_heading: "contributions"

# it.yaml
contrib_widget_label: "Attività di contribuzione nell'ultimo anno"
contrib_heading: "contribuzioni"
```

### JS translation map (bundled in `contribution-graph.js`)
```js
const I18N = {
  en: {
    summary: (t, g, h) => `${t} commits in the last year · ${g} from gitolite · ${h} from GitHub`,
    gitolite: 'gitolite',
    github: 'GitHub',
  },
  it: {
    summary: (t, g, h) => `${t} commit nell'ultimo anno · ${g} da gitolite · ${h} da GitHub`,
    gitolite: 'gitolite',
    github: 'GitHub',
  },
};
```

The partial passes `.Lang` as `data-lang` on the container; JS reads it to pick the correct translation.

---

## Accessibility

- Grid wrapped in `<figure role="img" aria-label="[contrib_widget_label]">`
- Summary line in `<figcaption>` — provides text equivalent of graph (WCAG 1.1.1)
- Each cell: `aria-label="[date]: [g] gitolite, [h] github"` — keyboard/SR navigable
- Tooltip: `aria-hidden="true"` — decorative only
- No color as sole information carrier — aria-labels and summary line provide all data textually
- No tooltip transition → no `prefers-reduced-motion` concern
- Cells are NOT individually tab-focusable (365 tab stops is unusable); tooltip is mouse-only
- Screen readers navigate cells via virtual cursor using each cell's `aria-label`
- `<figure>` `aria-label` + `<figcaption>` summary provide complete information without requiring cell-level interaction

---

## Error Handling

- Fetch fails or JSON parse fails → container hidden silently (`display: none` stays)
- No error message shown to user

---

## Theming Compliance

- Zero hardcoded hex values — all colors via `--accent`, `--accent2`, `--border`, `--bg2`, `--text`, `--text-dim`, `--font-mono`
- Dark/light mode handled automatically by existing CSS var system
- `prefers-color-scheme` no-JS fallback: graph not shown (JS required) — acceptable for a JS-rendered widget
- Follows existing JS-per-partial pattern (same as `fortune.js`)

---

## Out of Scope

- JSON endpoint generation (already live)
- Canvas rendering
- Any Hugo template pre-rendering of grid cells
- Alpine.js usage
- CSS animations on cells or tooltip
