# 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 ``` --- ## 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