summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-05-08-contribution-graph-design.md
blob: e5aec147f15ce7bac0e0e124fa5454b43e7d5e6e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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