diff options
Diffstat (limited to 'assets')
| -rw-r--r-- | assets/css/main.css | 70 | ||||
| -rw-r--r-- | assets/css/main.min.css | 109 | ||||
| -rw-r--r-- | assets/js/contribution-graph.js | 246 |
3 files changed, 425 insertions, 0 deletions
diff --git a/assets/css/main.css b/assets/css/main.css index 3aafd4f..2455103 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -1923,3 +1923,73 @@ html.theme-light .prose-invert .cta-block a { vertical-align: -0.1em; margin: 0; } + +/* ============================================================ + Contribution Graph Widget + ============================================================ */ + +.contrib-graph-wrap { + overflow-x: auto; +} + +.contrib-month-row { + display: grid; + grid-auto-flow: column; + gap: var(--contrib-gap, 2px); + margin-bottom: 2px; + width: fit-content; + min-width: 100%; +} + +.contrib-month-label { + font-size: 0.55rem; + color: var(--text-dim); + font-family: monospace; + white-space: nowrap; + overflow: hidden; +} + +.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; + line-height: 1.5; + color: var(--text); + font-family: monospace; + pointer-events: none; + z-index: 50; + white-space: nowrap; + display: none; +} + +.contrib-tooltip.visible { + display: block; +} + +.contrib-summary { + text-align: center; + font-size: 0.65rem; + color: var(--text-dim); + margin-top: 6px; + font-family: monospace; + display: block; +} diff --git a/assets/css/main.min.css b/assets/css/main.min.css index 0495346..056f17d 100644 --- a/assets/css/main.min.css +++ b/assets/css/main.min.css @@ -2120,6 +2120,10 @@ article.border.border-border\/30.rounded-lg.overflow-hidden.group.menu-overlay { opacity: 1; } +.contrib-tooltip.menu-overlay.active { + display: block; +} + /* Breadcrumb navigation */ .breadcrumb { @@ -2491,6 +2495,10 @@ article.border.border-border\/30.rounded-lg.overflow-hidden.group.menu-overlay { display: inline-flex; } +.\!grid { + display: grid !important; +} + .grid { display: grid; } @@ -3854,6 +3862,10 @@ select:focus-visible { opacity: 1; } +.contrib-tooltip.modal-backdrop.active { + display: block; +} + /* Modal container */ .modal { @@ -3875,12 +3887,26 @@ select:focus-visible { .modal.active { visibility: visible; opacity: 1; +} + +.contrib-tooltip.modal.active { + display: block; +} + +.modal.active { pointer-events: auto; } .modal.active .modal-backdrop { visibility: visible; opacity: 1; +} + +.contrib-tooltip.modal.active .modal-backdrop { + display: block; +} + +.modal.active .modal-backdrop { pointer-events: auto; } @@ -4318,6 +4344,10 @@ article.toast.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg { opacity: 1; } +.contrib-tooltip.tooltip:hover .tooltip-text { + display: block; +} + .tooltip-text::after { content: ''; position: absolute; @@ -4594,6 +4624,13 @@ html.theme-light .cta-block { box-shadow: none; } +/* Beat html.theme-light .prose-invert a { color: var(--accent) } which ignores not-prose */ + +html.theme-light .prose .cta-block a, +html.theme-light .prose-invert .cta-block a { + color: var(--on-accent); +} + /* Motion Safety - Respect prefers-reduced-motion */ @media (prefers-reduced-motion: reduce) { @@ -4624,6 +4661,78 @@ html.theme-light .cta-block { margin: 0; } +/* ============================================================ + Contribution Graph Widget + ============================================================ */ + +.contrib-graph-wrap { + overflow-x: auto; +} + +.contrib-month-row { + display: grid; + grid-auto-flow: column; + gap: var(--contrib-gap, 2px); + margin-bottom: 2px; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; +} + +.contrib-month-label { + font-size: 0.55rem; + color: var(--text-dim); + font-family: monospace; + white-space: nowrap; + overflow: hidden; +} + +.contrib-grid { + display: grid; + grid-template-rows: repeat(7, var(--contrib-cell-size, 8px)); + grid-auto-flow: column; + gap: var(--contrib-gap, 2px); + width: -moz-fit-content; + 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; + line-height: 1.5; + color: var(--text); + font-family: monospace; + pointer-events: none; + z-index: 50; + white-space: nowrap; + display: none; +} + +.contrib-tooltip.visible { + display: block; +} + +.contrib-summary { + text-align: center; + font-size: 0.65rem; + color: var(--text-dim); + margin-top: 6px; + font-family: monospace; + display: block; +} + .hover\:bg-surface:hover { background-color: var(--surface); } diff --git a/assets/js/contribution-graph.js b/assets/js/contribution-graph.js new file mode 100644 index 0000000..cd6dfdc --- /dev/null +++ b/assets/js/contribution-graph.js @@ -0,0 +1,246 @@ +(function () { + 'use strict'; + + if (window._contribGraphLoaded) return; + window._contribGraphLoaded = true; + + var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + + var I18N = { + en: { + summary: function(t, g, h) { return t + ' commits in the last year \xb7 ' + g + ' from gitolite \xb7 ' + h + ' from GitHub'; }, + tooltip_gitolite: 'gitolite', + tooltip_github: 'GitHub', + tooltip_total: 'total', + }, + it: { + summary: function(t, g, h) { return t + ' commit nell’ultimo anno \xb7 ' + g + ' da gitolite \xb7 ' + h + ' da GitHub'; }, + tooltip_gitolite: 'gitolite', + tooltip_github: 'GitHub', + tooltip_total: 'totale', + }, + }; + + function level(count) { + if (count === 0) return 0; + if (count <= 3) return 0.35; + if (count <= 9) return 0.60; + if (count <= 19) return 0.80; + return 1.0; + } + + function hexWithOpacity(hex, opacity) { + hex = hex.replace('#', '').trim(); + if (hex.length === 3) { + hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + } + var r = parseInt(hex.substring(0, 2), 16); + var g = parseInt(hex.substring(2, 4), 16); + var b = parseInt(hex.substring(4, 6), 16); + return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')'; + } + + function buildCellStyle(gitoliteCount, githubCount, rootStyle) { + var gLevel = level(gitoliteCount); + var hLevel = level(githubCount); + + if (gLevel === 0 && hLevel === 0) return ''; + + var accent2 = rootStyle.getPropertyValue('--accent2').trim() || '#00ff88'; + var accent = rootStyle.getPropertyValue('--accent').trim() || '#a855f7'; + var border = rootStyle.getPropertyValue('--border').trim() || '#182840'; + + var leftColor = gLevel === 0 ? border : hexWithOpacity(accent2, gLevel); + var rightColor = hLevel === 0 ? border : hexWithOpacity(accent, hLevel); + + return 'linear-gradient(90deg, ' + leftColor + ' 50%, ' + rightColor + ' 50%)'; + } + + function formatDate(dateStr) { + // dateStr: "YYYY-MM-DD" + var parts = dateStr.split('-'); + var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); + var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + var day = d.getDate(); + return months[d.getMonth()] + ' ' + day + ', ' + d.getFullYear(); + } + + function buildGraph(container, data) { + var cellSize = parseInt(container.dataset.cellSize, 10) || 8; + var lang = container.dataset.lang || 'en'; + var ariaLabel = container.dataset.ariaLabel || 'Contribution graph'; + var i18n = I18N[lang] || I18N.en; + var gap = cellSize <= 6 ? 1 : 2; + + var rootStyle = getComputedStyle(document.documentElement); + + // Sort dates oldest → newest + var dates = Object.keys(data).sort(); + + // Build 53-col × 7-row grid + // Find the day-of-week of the oldest date (Sun=0) + var firstParts = dates[0].split('-'); + var firstDate = new Date(parseInt(firstParts[0]), parseInt(firstParts[1]) - 1, parseInt(firstParts[2])); + var startDow = firstDate.getDay(); // 0=Sun + + // Assign each date to (col, row) + // col 0 starts at row=startDow + var cells = []; // { col, row, date, gitolite, github } + for (var i = 0; i < dates.length; i++) { + var slot = startDow + i; + var col = Math.floor(slot / 7); + var row = slot % 7; + cells.push({ col: col, row: row, date: dates[i], gitolite: data[dates[i]].gitolite || 0, github: data[dates[i]].github || 0 }); + } + + var totalCols = cells[cells.length - 1].col + 1; + + // Month labels: track first col per month + var monthCols = {}; // "YYYY-MM" → col index + for (var j = 0; j < cells.length; j++) { + var c = cells[j]; + var ym = c.date.substring(0, 7); + if (!(ym in monthCols)) monthCols[ym] = c.col; + } + + // Build figure + var figure = document.createElement('figure'); + figure.setAttribute('role', 'img'); + figure.setAttribute('aria-label', ariaLabel); + + var wrap = document.createElement('div'); + wrap.className = 'contrib-graph-wrap'; + + // Month label row + var monthRow = document.createElement('div'); + monthRow.className = 'contrib-month-row'; + monthRow.style.setProperty('--contrib-cell-size', cellSize + 'px'); + monthRow.style.setProperty('--contrib-gap', gap + 'px'); + monthRow.style.gridTemplateColumns = 'repeat(' + totalCols + ', ' + cellSize + 'px)'; + + var monthKeys = Object.keys(monthCols).sort(); + for (var m = 0; m < monthKeys.length; m++) { + var ym2 = monthKeys[m]; + var colIdx = monthCols[ym2]; + var monthNum = parseInt(ym2.split('-')[1], 10) - 1; + var span = document.createElement('span'); + span.className = 'contrib-month-label'; + span.textContent = MONTHS[monthNum]; + span.style.gridColumn = (colIdx + 1).toString(); + monthRow.appendChild(span); + } + + // Graph grid + var grid = document.createElement('div'); + grid.className = 'contrib-grid'; + grid.style.setProperty('--contrib-cell-size', cellSize + 'px'); + grid.style.setProperty('--contrib-gap', gap + 'px'); + grid.style.gridTemplateColumns = 'repeat(' + totalCols + ', ' + cellSize + 'px)'; + + // Build a map of col+row → cell data for quick lookup + var cellMap = {}; + for (var k = 0; k < cells.length; k++) { + cellMap[cells[k].col + '_' + cells[k].row] = cells[k]; + } + + // Fill grid col by col, row by row + for (var col2 = 0; col2 < totalCols; col2++) { + for (var row2 = 0; row2 < 7; row2++) { + var div = document.createElement('div'); + div.className = 'contrib-cell'; + var key = col2 + '_' + row2; + var cellData = cellMap[key]; + if (cellData) { + var bg = buildCellStyle(cellData.gitolite, cellData.github, rootStyle); + if (bg) div.style.background = bg; + var label = formatDate(cellData.date) + ': ' + cellData.gitolite + ' ' + i18n.tooltip_gitolite + ', ' + cellData.github + ' ' + i18n.tooltip_github; + div.setAttribute('aria-label', label); + div.dataset.gitolite = cellData.gitolite; + div.dataset.github = cellData.github; + div.dataset.date = cellData.date; + } + grid.appendChild(div); + } + } + + wrap.appendChild(monthRow); + wrap.appendChild(grid); + figure.appendChild(wrap); + + // Totals for summary + var totalCommits = 0, totalGitolite = 0, totalGithub = 0; + for (var n = 0; n < cells.length; n++) { + totalGitolite += cells[n].gitolite; + totalGithub += cells[n].github; + } + totalCommits = totalGitolite + totalGithub; + + // Summary figcaption + var caption = document.createElement('figcaption'); + caption.className = 'contrib-summary'; + caption.textContent = i18n.summary(totalCommits, totalGitolite, totalGithub); + figure.appendChild(caption); + + container.appendChild(figure); + container.style.display = ''; + + // Tooltip (inject once) + var tooltip = document.querySelector('.contrib-tooltip'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'contrib-tooltip'; + tooltip.setAttribute('aria-hidden', 'true'); + document.body.appendChild(tooltip); + } + + // Event delegation on grid + grid.addEventListener('mouseover', function(e) { + var cell = e.target.closest('.contrib-cell'); + if (!cell || !cell.dataset.date) return; + var g = parseInt(cell.dataset.gitolite, 10); + var h = parseInt(cell.dataset.github, 10); + tooltip.innerHTML = + formatDate(cell.dataset.date) + '<br>' + + i18n.tooltip_gitolite + ': ' + g + '<br>' + + i18n.tooltip_github + ': ' + h + '<br>' + + i18n.tooltip_total + ': ' + (g + h); + tooltip.classList.add('visible'); + }); + + grid.addEventListener('mousemove', function(e) { + var x = e.clientX + 12; + var y = e.clientY + 12; + var tw = tooltip.offsetWidth; + var th = tooltip.offsetHeight; + if (x + tw > window.innerWidth - 8) x = e.clientX - tw - 12; + if (y + th > window.innerHeight - 8) y = e.clientY - th - 12; + tooltip.style.left = x + 'px'; + tooltip.style.top = y + 'px'; + }); + + grid.addEventListener('mouseout', function(e) { + if (!e.relatedTarget || !grid.contains(e.relatedTarget)) { + tooltip.classList.remove('visible'); + } + }); + } + + document.addEventListener('DOMContentLoaded', function() { + var containers = document.querySelectorAll('.contrib-container'); + if (!containers.length) return; + + fetch('https://danix.xyz/contributions.json') + .then(function(res) { + if (!res.ok) throw new Error('fetch failed'); + return res.json(); + }) + .then(function(data) { + containers.forEach(function(container) { + buildGraph(container, data); + }); + }) + .catch(function() { + // hide silently — containers already display:none + }); + }); +}()); |
