diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/specs/2026-05-08-contribution-graph-design.md | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-05-08-contribution-graph-design.md b/docs/superpowers/specs/2026-05-08-contribution-graph-design.md new file mode 100644 index 0000000..e5aec14 --- /dev/null +++ b/docs/superpowers/specs/2026-05-08-contribution-graph-design.md @@ -0,0 +1,286 @@ +# 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 |
