@rsalianto/git-heatmap-vanilla 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.iife.js +138 -131
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +138 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +138 -131
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
DAY_LABELS,
|
|
7
7
|
DEFAULT_LEVELS
|
|
8
8
|
} from "@rsalianto/git-heatmap-core";
|
|
9
|
+
var DAY_LABEL_W = 32;
|
|
10
|
+
var MONTH_LABEL_H = 16;
|
|
9
11
|
var BASE_STYLES = `
|
|
10
12
|
:host {
|
|
11
13
|
--ghm-color-l0: rgba(255,255,255,0.08);
|
|
@@ -30,16 +32,9 @@ var BASE_STYLES = `
|
|
|
30
32
|
}
|
|
31
33
|
* { box-sizing: border-box; }
|
|
32
34
|
.ghm-scroll { overflow-x: auto; overflow-y: visible; padding-bottom: 4px; }
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.ghm-cell {
|
|
37
|
-
border-radius: 2px;
|
|
38
|
-
cursor: pointer;
|
|
39
|
-
transition: opacity 0.15s;
|
|
40
|
-
flex-shrink: 0;
|
|
41
|
-
}
|
|
42
|
-
.ghm-cell:hover { opacity: 0.7; }
|
|
35
|
+
svg { display: block; overflow: visible; }
|
|
36
|
+
rect { transition: opacity 0.15s; cursor: pointer; }
|
|
37
|
+
rect:hover { opacity: 0.7; }
|
|
43
38
|
.ghm-tooltip {
|
|
44
39
|
pointer-events: none;
|
|
45
40
|
position: absolute;
|
|
@@ -53,14 +48,17 @@ var BASE_STYLES = `
|
|
|
53
48
|
padding: 3px 8px;
|
|
54
49
|
white-space: nowrap;
|
|
55
50
|
}
|
|
56
|
-
.ghm-skeleton { opacity: 0.4; }
|
|
57
51
|
`;
|
|
52
|
+
function svgEl(tag, attrs = {}) {
|
|
53
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
54
|
+
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
|
|
55
|
+
return el;
|
|
56
|
+
}
|
|
58
57
|
var GitHeatmapElement = class extends HTMLElement {
|
|
59
58
|
constructor() {
|
|
60
59
|
super();
|
|
61
60
|
this._data = null;
|
|
62
61
|
this._fetchData = null;
|
|
63
|
-
this._tooltip = null;
|
|
64
62
|
this._shadow = this.attachShadow({ mode: "open" });
|
|
65
63
|
}
|
|
66
64
|
static get observedAttributes() {
|
|
@@ -79,15 +77,11 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
79
77
|
}
|
|
80
78
|
connectedCallback() {
|
|
81
79
|
const apiUrl = this.getAttribute("api-url");
|
|
82
|
-
if (apiUrl && !this._data)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this._render();
|
|
86
|
-
} else {
|
|
87
|
-
this._renderSkeleton();
|
|
88
|
-
}
|
|
80
|
+
if (apiUrl && !this._data) this._load();
|
|
81
|
+
else if (this._data) this._render();
|
|
82
|
+
else this._renderSkeleton();
|
|
89
83
|
}
|
|
90
|
-
attributeChangedCallback(name
|
|
84
|
+
attributeChangedCallback(name) {
|
|
91
85
|
if (name === "api-url") {
|
|
92
86
|
this._load();
|
|
93
87
|
return;
|
|
@@ -121,6 +115,12 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
121
115
|
get _label() {
|
|
122
116
|
return this.getAttribute("label") ?? "Contribution heatmap";
|
|
123
117
|
}
|
|
118
|
+
get _offsetX() {
|
|
119
|
+
return this._showDays ? DAY_LABEL_W : 0;
|
|
120
|
+
}
|
|
121
|
+
get _offsetY() {
|
|
122
|
+
return this._showMonths ? MONTH_LABEL_H + this._cellGap : 0;
|
|
123
|
+
}
|
|
124
124
|
async _load() {
|
|
125
125
|
const apiUrl = this.getAttribute("api-url");
|
|
126
126
|
const resolver = this._fetchData ?? (apiUrl ? () => fetch(apiUrl).then((r) => r.json()) : null);
|
|
@@ -134,133 +134,140 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
_renderSkeleton() {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;
|
|
138
|
+
const w = 53 * step + ox;
|
|
139
|
+
const h = 7 * step - cg + oy;
|
|
140
|
+
const svg = svgEl("svg", { width: w, height: h });
|
|
141
|
+
for (let wi = 0; wi < 53; wi++) {
|
|
142
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
143
|
+
svg.appendChild(svgEl("rect", { x: ox + wi * step, y: oy + dow * step, width: cs, height: cs, rx: cr, fill: "var(--ghm-color-l0)" }));
|
|
143
144
|
}
|
|
144
|
-
cols += `<div style="display:flex;flex-direction:column;gap:${gap}px;">${cells}</div>`;
|
|
145
145
|
}
|
|
146
|
-
this._shadow.innerHTML = `<style>${BASE_STYLES}</style
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;
|
|
147
|
+
const wrap = document.createElement("div");
|
|
148
|
+
wrap.style.opacity = "0.4";
|
|
149
|
+
wrap.appendChild(svg);
|
|
150
|
+
this._shadow.appendChild(wrap);
|
|
150
151
|
}
|
|
151
152
|
_renderError() {
|
|
152
|
-
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>
|
|
153
|
-
<p style="opacity:0.6;padding:1rem 0;">Could not load contribution data.</p>`;
|
|
153
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style><p style="opacity:0.6;padding:1rem 0;">Could not load contribution data.</p>`;
|
|
154
154
|
}
|
|
155
155
|
_render() {
|
|
156
156
|
if (!this._data) return;
|
|
157
157
|
const d = this._data;
|
|
158
|
-
const
|
|
158
|
+
const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;
|
|
159
159
|
const levels = DEFAULT_LEVELS;
|
|
160
|
-
const
|
|
161
|
-
|
|
160
|
+
const svgW = d.weeks.length * step + ox;
|
|
161
|
+
const svgH = 7 * step - cg + oy;
|
|
162
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;
|
|
163
|
+
const wrapper = document.createElement("div");
|
|
164
|
+
wrapper.setAttribute("aria-label", this._label);
|
|
165
|
+
wrapper.style.position = "relative";
|
|
166
|
+
if (this._showTotal) {
|
|
167
|
+
const p = document.createElement("p");
|
|
168
|
+
p.style.cssText = "margin-bottom:12px; color:var(--ghm-text);";
|
|
169
|
+
p.textContent = `${d.totalContributions.toLocaleString()} contributions in the last year`;
|
|
170
|
+
wrapper.appendChild(p);
|
|
171
|
+
}
|
|
172
|
+
const scroll = document.createElement("div");
|
|
173
|
+
scroll.className = "ghm-scroll";
|
|
174
|
+
const svg = svgEl("svg", { width: svgW, height: svgH, role: "img", "aria-label": this._label });
|
|
162
175
|
if (this._showMonths) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
const labels = buildMonthLabels(d.weeks);
|
|
177
|
+
for (const { label: ml, col } of labels) {
|
|
178
|
+
const t = svgEl("text", { x: ox + col * step, y: MONTH_LABEL_H - 4, fill: "var(--ghm-text)", "font-size": 10 });
|
|
179
|
+
t.textContent = ml;
|
|
180
|
+
svg.appendChild(t);
|
|
181
|
+
}
|
|
168
182
|
}
|
|
169
|
-
let dayLabelCol = "";
|
|
170
183
|
if (this._showDays) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
(
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
185
|
+
const lbl = DAY_LABELS[dow];
|
|
186
|
+
if (!lbl) continue;
|
|
187
|
+
const t = svgEl("text", { x: DAY_LABEL_W - 6, y: oy + dow * step + cs, fill: "var(--ghm-text)", "font-size": 10, "text-anchor": "end" });
|
|
188
|
+
t.textContent = lbl;
|
|
189
|
+
svg.appendChild(t);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const mobileTip = document.createElement("div");
|
|
193
|
+
mobileTip.style.cssText = "display:none; margin-top:8px; font-size:0.9em; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:4px; padding:6px 12px;";
|
|
194
|
+
let activeTapDate = "";
|
|
195
|
+
const tt = document.createElement("div");
|
|
196
|
+
tt.className = "ghm-tooltip";
|
|
197
|
+
tt.style.display = "none";
|
|
198
|
+
for (let wi = 0; wi < d.weeks.length; wi++) {
|
|
199
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
200
|
+
const day = d.weeks[wi].days[dow];
|
|
201
|
+
if (!day?.date) continue;
|
|
202
|
+
const rect = svgEl("rect", {
|
|
203
|
+
x: ox + wi * step,
|
|
204
|
+
y: oy + dow * step,
|
|
205
|
+
width: cs,
|
|
206
|
+
height: cs,
|
|
207
|
+
rx: cr,
|
|
208
|
+
fill: `var(--ghm-color-l${day.level})`
|
|
209
|
+
});
|
|
210
|
+
rect.setAttribute("aria-label", this._tipText(day));
|
|
211
|
+
rect.setAttribute("role", "button");
|
|
212
|
+
rect.addEventListener("mouseenter", (e) => {
|
|
213
|
+
const r = rect.getBoundingClientRect();
|
|
214
|
+
const wr = wrapper.getBoundingClientRect();
|
|
215
|
+
tt.textContent = this._tipText(day);
|
|
216
|
+
tt.style.left = `${r.left - wr.left + cs / 2}px`;
|
|
217
|
+
tt.style.top = `${r.top - wr.top - 6}px`;
|
|
218
|
+
tt.style.display = "block";
|
|
219
|
+
});
|
|
220
|
+
rect.addEventListener("mouseleave", () => {
|
|
221
|
+
tt.style.display = "none";
|
|
222
|
+
});
|
|
223
|
+
rect.addEventListener("click", () => {
|
|
224
|
+
if (activeTapDate === day.date) {
|
|
225
|
+
activeTapDate = "";
|
|
226
|
+
mobileTip.style.display = "none";
|
|
227
|
+
} else {
|
|
228
|
+
activeTapDate = day.date;
|
|
229
|
+
mobileTip.textContent = this._tipText(day);
|
|
230
|
+
mobileTip.style.display = "block";
|
|
231
|
+
}
|
|
232
|
+
this.dispatchEvent(new CustomEvent("day-click", { detail: { date: day.date, count: day.count }, bubbles: true, composed: true }));
|
|
233
|
+
});
|
|
234
|
+
svg.appendChild(rect);
|
|
235
|
+
}
|
|
176
236
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return `<div
|
|
184
|
-
class="ghm-cell"
|
|
185
|
-
style="width:${cell}px;height:${cell}px;border-radius:${r}px;background:var(--ghm-color-l${lvl});"
|
|
186
|
-
data-date="${day.date}"
|
|
187
|
-
data-count="${day.count}"
|
|
188
|
-
aria-label="${tip}"
|
|
189
|
-
role="button"
|
|
190
|
-
tabindex="0"
|
|
191
|
-
></div>`;
|
|
192
|
-
}).join("");
|
|
193
|
-
return `<div style="display:flex;flex-direction:column;gap:${gap}px;">${cells}</div>`;
|
|
194
|
-
}).join("");
|
|
195
|
-
let legend = "";
|
|
237
|
+
scroll.appendChild(svg);
|
|
238
|
+
wrapper.appendChild(scroll);
|
|
239
|
+
requestAnimationFrame(() => {
|
|
240
|
+
scroll.scrollLeft = scroll.scrollWidth;
|
|
241
|
+
});
|
|
242
|
+
wrapper.appendChild(mobileTip);
|
|
196
243
|
if (this._showLegend) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
244
|
+
const legend = document.createElement("div");
|
|
245
|
+
legend.style.cssText = "display:flex; align-items:center; justify-content:flex-end; margin-top:8px;";
|
|
246
|
+
const inner = document.createElement("div");
|
|
247
|
+
inner.style.cssText = "display:flex; align-items:center; gap:6px; font-size:0.9em;";
|
|
248
|
+
const less = document.createElement("span");
|
|
249
|
+
less.textContent = "Less";
|
|
250
|
+
const more = document.createElement("span");
|
|
251
|
+
more.textContent = "More";
|
|
252
|
+
inner.appendChild(less);
|
|
253
|
+
for (let i = 0; i < levels.length; i++) {
|
|
254
|
+
const s = svgEl("svg", { width: cs, height: cs });
|
|
255
|
+
const r = svgEl("rect", { width: cs, height: cs, rx: cr, fill: `var(--ghm-color-l${i})` });
|
|
256
|
+
const title = svgEl("title");
|
|
257
|
+
title.textContent = levels[i].label;
|
|
258
|
+
r.appendChild(title);
|
|
259
|
+
s.appendChild(r);
|
|
260
|
+
inner.appendChild(s);
|
|
261
|
+
}
|
|
262
|
+
inner.appendChild(more);
|
|
263
|
+
legend.appendChild(inner);
|
|
264
|
+
wrapper.appendChild(legend);
|
|
205
265
|
}
|
|
206
|
-
|
|
207
|
-
this._shadow.
|
|
208
|
-
<div aria-label="${this._label}" style="position:relative;">
|
|
209
|
-
${total}
|
|
210
|
-
<div class="ghm-scroll">
|
|
211
|
-
<div class="ghm-grid" style="min-width:${d.weeks.length * step}px;">
|
|
212
|
-
${monthRow}
|
|
213
|
-
<div style="display:flex;">
|
|
214
|
-
${dayLabelCol}
|
|
215
|
-
<div style="display:flex;gap:${gap}px;">${weekCols}</div>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
<div id="ghm-mobile-tip" style="display:none;margin-top:8px;font-size:0.9em;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:4px;padding:6px 12px;"></div>
|
|
220
|
-
${legend}
|
|
221
|
-
<div class="ghm-tooltip" id="ghm-tt" style="display:none;"></div>
|
|
222
|
-
</div>`;
|
|
223
|
-
this._attachListeners();
|
|
224
|
-
const scrollEl = this._shadow.querySelector(".ghm-scroll");
|
|
225
|
-
if (scrollEl) scrollEl.scrollLeft = scrollEl.scrollWidth;
|
|
266
|
+
wrapper.appendChild(tt);
|
|
267
|
+
this._shadow.appendChild(wrapper);
|
|
226
268
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const tt = this._shadow.getElementById("ghm-tt");
|
|
230
|
-
const mobileTip = this._shadow.getElementById("ghm-mobile-tip");
|
|
231
|
-
let activeTapDate = "";
|
|
232
|
-
this._shadow.querySelectorAll(".ghm-cell").forEach((el) => {
|
|
233
|
-
const cell = el;
|
|
234
|
-
const date = cell.dataset["date"] ?? "";
|
|
235
|
-
const count = parseInt(cell.dataset["count"] ?? "0", 10);
|
|
236
|
-
const tip = count === 0 ? `No contributions on ${date}` : `${count} contribution${count > 1 ? "s" : ""} on ${date}`;
|
|
237
|
-
cell.addEventListener("mouseenter", (e) => {
|
|
238
|
-
const r = cell.getBoundingClientRect();
|
|
239
|
-
const wr = wrapper.getBoundingClientRect();
|
|
240
|
-
tt.textContent = tip;
|
|
241
|
-
tt.style.left = `${r.left - wr.left + this._cellSize / 2}px`;
|
|
242
|
-
tt.style.top = `${r.top - wr.top - 6}px`;
|
|
243
|
-
tt.style.display = "block";
|
|
244
|
-
});
|
|
245
|
-
cell.addEventListener("mouseleave", () => {
|
|
246
|
-
tt.style.display = "none";
|
|
247
|
-
});
|
|
248
|
-
cell.addEventListener("click", () => {
|
|
249
|
-
if (activeTapDate === date) {
|
|
250
|
-
activeTapDate = "";
|
|
251
|
-
mobileTip.style.display = "none";
|
|
252
|
-
} else {
|
|
253
|
-
activeTapDate = date;
|
|
254
|
-
mobileTip.textContent = tip;
|
|
255
|
-
mobileTip.style.display = "block";
|
|
256
|
-
}
|
|
257
|
-
this.dispatchEvent(new CustomEvent("day-click", {
|
|
258
|
-
detail: { date, count },
|
|
259
|
-
bubbles: true,
|
|
260
|
-
composed: true
|
|
261
|
-
}));
|
|
262
|
-
});
|
|
263
|
-
});
|
|
269
|
+
_tipText(day) {
|
|
270
|
+
return day.count === 0 ? `No contributions on ${day.date}` : `${day.count} contribution${day.count > 1 ? "s" : ""} on ${day.date}`;
|
|
264
271
|
}
|
|
265
272
|
};
|
|
266
273
|
if (typeof customElements !== "undefined" && !customElements.get("git-heatmap")) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/git-heatmap-element.ts"],"sourcesContent":["import {\n buildMonthLabels, CELL, GAP, STEP, DAY_LABELS,\n DEFAULT_LEVELS, DEFAULT_THEME, getLevel,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, LevelConfig } from \"@rsalianto/git-heatmap-core\";\n\nconst BASE_STYLES = `\n :host {\n --ghm-color-l0: rgba(255,255,255,0.08);\n --ghm-color-l1: #1c3d06;\n --ghm-color-l2: #3a7510;\n --ghm-color-l3: #6ab81e;\n --ghm-color-l4: #aafd35;\n --ghm-text: rgba(255,255,255,0.5);\n --ghm-tooltip-bg: #1c2128;\n --ghm-tooltip-border: rgba(255,255,255,0.1);\n --ghm-tooltip-text: rgba(255,255,255,0.75);\n --ghm-font: inherit;\n --ghm-fs: 11px;\n display: block;\n position: relative;\n width: 100%;\n font-size: var(--ghm-fs);\n font-family: var(--ghm-font);\n color: var(--ghm-text);\n user-select: none;\n box-sizing: border-box;\n }\n * { box-sizing: border-box; }\n .ghm-scroll { overflow-x: auto; overflow-y: visible; padding-bottom: 4px; }\n .ghm-grid { display: inline-flex; flex-direction: column; }\n .ghm-row { display: flex; }\n .ghm-col { display: flex; flex-direction: column; }\n .ghm-cell {\n border-radius: 2px;\n cursor: pointer;\n transition: opacity 0.15s;\n flex-shrink: 0;\n }\n .ghm-cell:hover { opacity: 0.7; }\n .ghm-tooltip {\n pointer-events: none;\n position: absolute;\n z-index: 50;\n transform: translate(-50%, -100%);\n background: var(--ghm-tooltip-bg);\n border: 1px solid var(--ghm-tooltip-border);\n color: var(--ghm-tooltip-text);\n font-size: 0.9em;\n border-radius: 4px;\n padding: 3px 8px;\n white-space: nowrap;\n }\n .ghm-skeleton { opacity: 0.4; }\n`;\n\nexport class GitHeatmapElement extends HTMLElement {\n static get observedAttributes() {\n return [\"api-url\", \"cell-size\", \"cell-gap\", \"cell-radius\", \"show-total\", \"show-legend\", \"show-month-labels\", \"show-day-labels\", \"label\"];\n }\n\n private _data: HeatmapData | null = null;\n private _fetchData: (() => Promise<HeatmapData>) | null = null;\n private _shadow: ShadowRoot;\n private _tooltip: HTMLDivElement | null = null;\n\n set data(value: HeatmapData) { this._data = value; this._render(); }\n get data(): HeatmapData | null { return this._data; }\n\n set fetchData(fn: () => Promise<HeatmapData>) { this._fetchData = fn; this._load(); }\n\n constructor() {\n super();\n this._shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n const apiUrl = this.getAttribute(\"api-url\");\n if (apiUrl && !this._data) {\n this._load();\n } else if (this._data) {\n this._render();\n } else {\n this._renderSkeleton();\n }\n }\n\n attributeChangedCallback(name: string, _old: string | null, _val: string | null) {\n if (name === \"api-url\") { this._load(); return; }\n if (this._data) this._render();\n }\n\n private get _cellSize() { return parseInt(this.getAttribute(\"cell-size\") ?? String(CELL), 10); }\n private get _cellGap() { return parseInt(this.getAttribute(\"cell-gap\") ?? String(GAP), 10); }\n private get _cellRadius(){ return parseInt(this.getAttribute(\"cell-radius\") ?? \"2\", 10); }\n private get _step() { return this._cellSize + this._cellGap; }\n private get _showTotal() { return this.getAttribute(\"show-total\") !== \"false\"; }\n private get _showLegend(){ return this.getAttribute(\"show-legend\") !== \"false\"; }\n private get _showMonths(){ return this.getAttribute(\"show-month-labels\") !== \"false\"; }\n private get _showDays() { return this.getAttribute(\"show-day-labels\") !== \"false\"; }\n private get _label() { return this.getAttribute(\"label\") ?? \"Contribution heatmap\"; }\n\n private async _load() {\n const apiUrl = this.getAttribute(\"api-url\");\n const resolver = this._fetchData\n ?? (apiUrl ? () => fetch(apiUrl).then((r) => r.json()) : null);\n if (!resolver) return;\n\n this._renderSkeleton();\n try {\n this._data = await resolver() as HeatmapData;\n this._render();\n } catch {\n this._renderError();\n }\n }\n\n private _renderSkeleton() {\n const cell = this._cellSize, gap = this._cellGap, step = this._step;\n let cols = \"\";\n for (let c = 0; c < 53; c++) {\n let cells = \"\";\n for (let r = 0; r < 7; r++) {\n cells += `<div style=\"width:${cell}px;height:${cell}px;border-radius:${this._cellRadius}px;background:var(--ghm-color-l0);flex-shrink:0;\"></div>`;\n }\n cols += `<div style=\"display:flex;flex-direction:column;gap:${gap}px;\">${cells}</div>`;\n }\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style>\n <div aria-label=\"${this._label}\" style=\"opacity:0.4;\">\n <div style=\"display:flex;gap:${gap}px;\">${cols}</div>\n </div>`;\n }\n\n private _renderError() {\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style>\n <p style=\"opacity:0.6;padding:1rem 0;\">Could not load contribution data.</p>`;\n }\n\n private _render() {\n if (!this._data) return;\n const d = this._data;\n const cell = this._cellSize, gap = this._cellGap, r = this._cellRadius, step = this._step;\n const levels: LevelConfig[] = DEFAULT_LEVELS;\n\n const monthLabels = this._showMonths ? buildMonthLabels(d.weeks) : [];\n\n let monthRow = \"\";\n if (this._showMonths) {\n const cells = monthLabels.map(({ label, col }, idx) => {\n const nextCol = monthLabels[idx + 1]?.col ?? d.weeks.length;\n return `<div style=\"width:${(nextCol - col) * step}px;white-space:nowrap;font-size:0.9em;\">${label}</div>`;\n }).join(\"\");\n monthRow = `<div style=\"display:flex;margin-bottom:${gap}px;margin-left:${this._showDays ? 32 : 0}px;\">${cells}</div>`;\n }\n\n let dayLabelCol = \"\";\n if (this._showDays) {\n const rows = Array.from({ length: 7 }, (_, dow) =>\n `<div style=\"height:${cell}px;line-height:${cell}px;width:26px;text-align:right;padding-right:4px;font-size:0.9em;\">${DAY_LABELS[dow] ?? \"\"}</div>`\n ).join(\"\");\n dayLabelCol = `<div style=\"display:flex;flex-direction:column;gap:${gap}px;margin-right:6px;\">${rows}</div>`;\n }\n\n const weekCols = d.weeks.map((week, _wi) => {\n const cells = Array.from({ length: 7 }, (_, dow) => {\n const day = week.days[dow];\n if (!day?.date) return `<div style=\"width:${cell}px;height:${cell}px;flex-shrink:0;\"></div>`;\n const lvl = day.level;\n const tip = day.count === 0\n ? `No contributions on ${day.date}`\n : `${day.count} contribution${day.count > 1 ? \"s\" : \"\"} on ${day.date}`;\n return `<div\n class=\"ghm-cell\"\n style=\"width:${cell}px;height:${cell}px;border-radius:${r}px;background:var(--ghm-color-l${lvl});\"\n data-date=\"${day.date}\"\n data-count=\"${day.count}\"\n aria-label=\"${tip}\"\n role=\"button\"\n tabindex=\"0\"\n ></div>`;\n }).join(\"\");\n return `<div style=\"display:flex;flex-direction:column;gap:${gap}px;\">${cells}</div>`;\n }).join(\"\");\n\n let legend = \"\";\n if (this._showLegend) {\n const cells = levels.map((lvl, i) =>\n `<div style=\"width:${cell}px;height:${cell}px;border-radius:${r}px;background:var(--ghm-color-l${i});\" title=\"${lvl.label}\"></div>`\n ).join(\"\");\n legend = `<div style=\"display:flex;align-items:center;justify-content:flex-end;margin-top:8px;\">\n <div style=\"display:flex;align-items:center;gap:6px;font-size:0.9em;\">\n <span>Less</span>${cells}<span>More</span>\n </div>\n </div>`;\n }\n\n const total = this._showTotal\n ? `<p style=\"margin-bottom:12px;color:var(--ghm-text);\">${d.totalContributions.toLocaleString()} contributions in the last year</p>`\n : \"\";\n\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style>\n <div aria-label=\"${this._label}\" style=\"position:relative;\">\n ${total}\n <div class=\"ghm-scroll\">\n <div class=\"ghm-grid\" style=\"min-width:${d.weeks.length * step}px;\">\n ${monthRow}\n <div style=\"display:flex;\">\n ${dayLabelCol}\n <div style=\"display:flex;gap:${gap}px;\">${weekCols}</div>\n </div>\n </div>\n </div>\n <div id=\"ghm-mobile-tip\" style=\"display:none;margin-top:8px;font-size:0.9em;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:4px;padding:6px 12px;\"></div>\n ${legend}\n <div class=\"ghm-tooltip\" id=\"ghm-tt\" style=\"display:none;\"></div>\n </div>`;\n\n this._attachListeners();\n\n // auto-scroll to end\n const scrollEl = this._shadow.querySelector(\".ghm-scroll\") as HTMLElement | null;\n if (scrollEl) scrollEl.scrollLeft = scrollEl.scrollWidth;\n }\n\n private _attachListeners() {\n const wrapper = this._shadow.firstElementChild as HTMLElement;\n const tt = this._shadow.getElementById(\"ghm-tt\") as HTMLDivElement;\n const mobileTip = this._shadow.getElementById(\"ghm-mobile-tip\") as HTMLDivElement;\n let activeTapDate = \"\";\n\n this._shadow.querySelectorAll(\".ghm-cell\").forEach((el) => {\n const cell = el as HTMLElement;\n const date = cell.dataset[\"date\"] ?? \"\";\n const count = parseInt(cell.dataset[\"count\"] ?? \"0\", 10);\n const tip = count === 0\n ? `No contributions on ${date}`\n : `${count} contribution${count > 1 ? \"s\" : \"\"} on ${date}`;\n\n cell.addEventListener(\"mouseenter\", (e) => {\n const r = cell.getBoundingClientRect();\n const wr = wrapper.getBoundingClientRect();\n tt.textContent = tip;\n tt.style.left = `${r.left - wr.left + this._cellSize / 2}px`;\n tt.style.top = `${r.top - wr.top - 6}px`;\n tt.style.display = \"block\";\n });\n cell.addEventListener(\"mouseleave\", () => { tt.style.display = \"none\"; });\n cell.addEventListener(\"click\", () => {\n if (activeTapDate === date) {\n activeTapDate = \"\";\n mobileTip.style.display = \"none\";\n } else {\n activeTapDate = date;\n mobileTip.textContent = tip;\n mobileTip.style.display = \"block\";\n }\n this.dispatchEvent(new CustomEvent(\"day-click\", {\n detail: { date, count },\n bubbles: true,\n composed: true,\n }));\n });\n });\n }\n}\n\nif (typeof customElements !== \"undefined\" && !customElements.get(\"git-heatmap\")) {\n customElements.define(\"git-heatmap\", GitHeatmapElement);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/git-heatmap-element.ts"],"sourcesContent":["import {\n buildMonthLabels, CELL, GAP, DAY_LABELS,\n DEFAULT_LEVELS,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, LevelConfig } from \"@rsalianto/git-heatmap-core\";\n\nconst DAY_LABEL_W = 32;\nconst MONTH_LABEL_H = 16;\n\nconst BASE_STYLES = `\n :host {\n --ghm-color-l0: rgba(255,255,255,0.08);\n --ghm-color-l1: #1c3d06;\n --ghm-color-l2: #3a7510;\n --ghm-color-l3: #6ab81e;\n --ghm-color-l4: #aafd35;\n --ghm-text: rgba(255,255,255,0.5);\n --ghm-tooltip-bg: #1c2128;\n --ghm-tooltip-border: rgba(255,255,255,0.1);\n --ghm-tooltip-text: rgba(255,255,255,0.75);\n --ghm-font: inherit;\n --ghm-fs: 11px;\n display: block;\n position: relative;\n width: 100%;\n font-size: var(--ghm-fs);\n font-family: var(--ghm-font);\n color: var(--ghm-text);\n user-select: none;\n box-sizing: border-box;\n }\n * { box-sizing: border-box; }\n .ghm-scroll { overflow-x: auto; overflow-y: visible; padding-bottom: 4px; }\n svg { display: block; overflow: visible; }\n rect { transition: opacity 0.15s; cursor: pointer; }\n rect:hover { opacity: 0.7; }\n .ghm-tooltip {\n pointer-events: none;\n position: absolute;\n z-index: 50;\n transform: translate(-50%, -100%);\n background: var(--ghm-tooltip-bg);\n border: 1px solid var(--ghm-tooltip-border);\n color: var(--ghm-tooltip-text);\n font-size: 0.9em;\n border-radius: 4px;\n padding: 3px 8px;\n white-space: nowrap;\n }\n`;\n\nfunction svgEl(tag: string, attrs: Record<string, string | number> = {}): SVGElement {\n const el = document.createElementNS(\"http://www.w3.org/2000/svg\", tag);\n for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));\n return el;\n}\n\nexport class GitHeatmapElement extends HTMLElement {\n static get observedAttributes() {\n return [\"api-url\", \"cell-size\", \"cell-gap\", \"cell-radius\", \"show-total\", \"show-legend\", \"show-month-labels\", \"show-day-labels\", \"label\"];\n }\n\n private _data: HeatmapData | null = null;\n private _fetchData: (() => Promise<HeatmapData>) | null = null;\n private _shadow: ShadowRoot;\n\n set data(value: HeatmapData) { this._data = value; this._render(); }\n get data(): HeatmapData | null { return this._data; }\n set fetchData(fn: () => Promise<HeatmapData>) { this._fetchData = fn; this._load(); }\n\n constructor() {\n super();\n this._shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n const apiUrl = this.getAttribute(\"api-url\");\n if (apiUrl && !this._data) this._load();\n else if (this._data) this._render();\n else this._renderSkeleton();\n }\n\n attributeChangedCallback(name: string) {\n if (name === \"api-url\") { this._load(); return; }\n if (this._data) this._render();\n }\n\n private get _cellSize() { return parseInt(this.getAttribute(\"cell-size\") ?? String(CELL), 10); }\n private get _cellGap() { return parseInt(this.getAttribute(\"cell-gap\") ?? String(GAP), 10); }\n private get _cellRadius(){ return parseInt(this.getAttribute(\"cell-radius\") ?? \"2\", 10); }\n private get _step() { return this._cellSize + this._cellGap; }\n private get _showTotal() { return this.getAttribute(\"show-total\") !== \"false\"; }\n private get _showLegend(){ return this.getAttribute(\"show-legend\") !== \"false\"; }\n private get _showMonths(){ return this.getAttribute(\"show-month-labels\") !== \"false\"; }\n private get _showDays() { return this.getAttribute(\"show-day-labels\") !== \"false\"; }\n private get _label() { return this.getAttribute(\"label\") ?? \"Contribution heatmap\"; }\n\n private get _offsetX() { return this._showDays ? DAY_LABEL_W : 0; }\n private get _offsetY() { return this._showMonths ? MONTH_LABEL_H + this._cellGap : 0; }\n\n private async _load() {\n const apiUrl = this.getAttribute(\"api-url\");\n const resolver = this._fetchData ?? (apiUrl ? () => fetch(apiUrl).then(r => r.json()) : null);\n if (!resolver) return;\n this._renderSkeleton();\n try {\n this._data = await resolver() as HeatmapData;\n this._render();\n } catch {\n this._renderError();\n }\n }\n\n private _renderSkeleton() {\n const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;\n const w = 53 * step + ox;\n const h = 7 * step - cg + oy;\n const svg = svgEl(\"svg\", { width: w, height: h });\n for (let wi = 0; wi < 53; wi++) {\n for (let dow = 0; dow < 7; dow++) {\n svg.appendChild(svgEl(\"rect\", { x: ox + wi * step, y: oy + dow * step, width: cs, height: cs, rx: cr, fill: \"var(--ghm-color-l0)\" }));\n }\n }\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;\n const wrap = document.createElement(\"div\");\n wrap.style.opacity = \"0.4\";\n wrap.appendChild(svg);\n this._shadow.appendChild(wrap);\n }\n\n private _renderError() {\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style><p style=\"opacity:0.6;padding:1rem 0;\">Could not load contribution data.</p>`;\n }\n\n private _render() {\n if (!this._data) return;\n const d = this._data;\n const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;\n const levels: LevelConfig[] = DEFAULT_LEVELS;\n\n const svgW = d.weeks.length * step + ox;\n const svgH = 7 * step - cg + oy;\n\n this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"aria-label\", this._label);\n wrapper.style.position = \"relative\";\n\n // Total\n if (this._showTotal) {\n const p = document.createElement(\"p\");\n p.style.cssText = \"margin-bottom:12px; color:var(--ghm-text);\";\n p.textContent = `${d.totalContributions.toLocaleString()} contributions in the last year`;\n wrapper.appendChild(p);\n }\n\n // Scroll + SVG\n const scroll = document.createElement(\"div\");\n scroll.className = \"ghm-scroll\";\n const svg = svgEl(\"svg\", { width: svgW, height: svgH, role: \"img\", \"aria-label\": this._label });\n\n // Month labels\n if (this._showMonths) {\n const labels = buildMonthLabels(d.weeks);\n for (const { label: ml, col } of labels) {\n const t = svgEl(\"text\", { x: ox + col * step, y: MONTH_LABEL_H - 4, fill: \"var(--ghm-text)\", \"font-size\": 10 });\n t.textContent = ml;\n svg.appendChild(t);\n }\n }\n\n // Day labels\n if (this._showDays) {\n for (let dow = 0; dow < 7; dow++) {\n const lbl = DAY_LABELS[dow];\n if (!lbl) continue;\n const t = svgEl(\"text\", { x: DAY_LABEL_W - 6, y: oy + dow * step + cs, fill: \"var(--ghm-text)\", \"font-size\": 10, \"text-anchor\": \"end\" });\n t.textContent = lbl;\n svg.appendChild(t);\n }\n }\n\n // Grid cells\n const mobileTip = document.createElement(\"div\");\n mobileTip.style.cssText = \"display:none; margin-top:8px; font-size:0.9em; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:4px; padding:6px 12px;\";\n let activeTapDate = \"\";\n\n const tt = document.createElement(\"div\");\n tt.className = \"ghm-tooltip\";\n tt.style.display = \"none\";\n\n for (let wi = 0; wi < d.weeks.length; wi++) {\n for (let dow = 0; dow < 7; dow++) {\n const day = d.weeks[wi].days[dow] as HeatmapDay | undefined;\n if (!day?.date) continue;\n const rect = svgEl(\"rect\", {\n x: ox + wi * step, y: oy + dow * step,\n width: cs, height: cs, rx: cr,\n fill: `var(--ghm-color-l${day.level})`,\n }) as SVGRectElement;\n rect.setAttribute(\"aria-label\", this._tipText(day));\n rect.setAttribute(\"role\", \"button\");\n\n rect.addEventListener(\"mouseenter\", (e) => {\n const r = rect.getBoundingClientRect();\n const wr = wrapper.getBoundingClientRect();\n tt.textContent = this._tipText(day);\n tt.style.left = `${r.left - wr.left + cs / 2}px`;\n tt.style.top = `${r.top - wr.top - 6}px`;\n tt.style.display = \"block\";\n });\n rect.addEventListener(\"mouseleave\", () => { tt.style.display = \"none\"; });\n rect.addEventListener(\"click\", () => {\n if (activeTapDate === day.date) {\n activeTapDate = \"\";\n mobileTip.style.display = \"none\";\n } else {\n activeTapDate = day.date;\n mobileTip.textContent = this._tipText(day);\n mobileTip.style.display = \"block\";\n }\n this.dispatchEvent(new CustomEvent(\"day-click\", { detail: { date: day.date, count: day.count }, bubbles: true, composed: true }));\n });\n\n svg.appendChild(rect);\n }\n }\n\n scroll.appendChild(svg);\n wrapper.appendChild(scroll);\n\n // Scroll to end\n requestAnimationFrame(() => { scroll.scrollLeft = scroll.scrollWidth; });\n\n wrapper.appendChild(mobileTip);\n\n // Legend\n if (this._showLegend) {\n const legend = document.createElement(\"div\");\n legend.style.cssText = \"display:flex; align-items:center; justify-content:flex-end; margin-top:8px;\";\n const inner = document.createElement(\"div\");\n inner.style.cssText = \"display:flex; align-items:center; gap:6px; font-size:0.9em;\";\n const less = document.createElement(\"span\"); less.textContent = \"Less\";\n const more = document.createElement(\"span\"); more.textContent = \"More\";\n inner.appendChild(less);\n for (let i = 0; i < levels.length; i++) {\n const s = svgEl(\"svg\", { width: cs, height: cs }) as SVGSVGElement & Element;\n const r = svgEl(\"rect\", { width: cs, height: cs, rx: cr, fill: `var(--ghm-color-l${i})` });\n const title = svgEl(\"title\"); title.textContent = levels[i].label;\n r.appendChild(title);\n s.appendChild(r);\n inner.appendChild(s as unknown as Node);\n }\n inner.appendChild(more);\n legend.appendChild(inner);\n wrapper.appendChild(legend);\n }\n\n wrapper.appendChild(tt);\n this._shadow.appendChild(wrapper);\n }\n\n private _tipText(day: HeatmapDay): string {\n return day.count === 0\n ? `No contributions on ${day.date}`\n : `${day.count} contribution${day.count > 1 ? \"s\" : \"\"} on ${day.date}`;\n }\n}\n\nif (typeof customElements !== \"undefined\" && !customElements.get(\"git-heatmap\")) {\n customElements.define(\"git-heatmap\", GitHeatmapElement);\n}\n"],"mappings":"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","names":[]}
|