# 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 `
` 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 ``, 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 `` 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
…month labels…
…
342 commits in the last year · 270 from gitolite · 72 from GitHub
```
---
## 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 ``
- Summary line in `` — 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`
- `` `aria-label` + `` 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