@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.js
CHANGED
|
@@ -26,6 +26,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
26
26
|
|
|
27
27
|
// src/git-heatmap-element.ts
|
|
28
28
|
var import_git_heatmap_core = require("@rsalianto/git-heatmap-core");
|
|
29
|
+
var DAY_LABEL_W = 32;
|
|
30
|
+
var MONTH_LABEL_H = 16;
|
|
29
31
|
var BASE_STYLES = `
|
|
30
32
|
:host {
|
|
31
33
|
--ghm-color-l0: rgba(255,255,255,0.08);
|
|
@@ -50,16 +52,9 @@ var BASE_STYLES = `
|
|
|
50
52
|
}
|
|
51
53
|
* { box-sizing: border-box; }
|
|
52
54
|
.ghm-scroll { overflow-x: auto; overflow-y: visible; padding-bottom: 4px; }
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.ghm-cell {
|
|
57
|
-
border-radius: 2px;
|
|
58
|
-
cursor: pointer;
|
|
59
|
-
transition: opacity 0.15s;
|
|
60
|
-
flex-shrink: 0;
|
|
61
|
-
}
|
|
62
|
-
.ghm-cell:hover { opacity: 0.7; }
|
|
55
|
+
svg { display: block; overflow: visible; }
|
|
56
|
+
rect { transition: opacity 0.15s; cursor: pointer; }
|
|
57
|
+
rect:hover { opacity: 0.7; }
|
|
63
58
|
.ghm-tooltip {
|
|
64
59
|
pointer-events: none;
|
|
65
60
|
position: absolute;
|
|
@@ -73,14 +68,17 @@ var BASE_STYLES = `
|
|
|
73
68
|
padding: 3px 8px;
|
|
74
69
|
white-space: nowrap;
|
|
75
70
|
}
|
|
76
|
-
.ghm-skeleton { opacity: 0.4; }
|
|
77
71
|
`;
|
|
72
|
+
function svgEl(tag, attrs = {}) {
|
|
73
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
74
|
+
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
|
|
75
|
+
return el;
|
|
76
|
+
}
|
|
78
77
|
var GitHeatmapElement = class extends HTMLElement {
|
|
79
78
|
constructor() {
|
|
80
79
|
super();
|
|
81
80
|
this._data = null;
|
|
82
81
|
this._fetchData = null;
|
|
83
|
-
this._tooltip = null;
|
|
84
82
|
this._shadow = this.attachShadow({ mode: "open" });
|
|
85
83
|
}
|
|
86
84
|
static get observedAttributes() {
|
|
@@ -99,15 +97,11 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
99
97
|
}
|
|
100
98
|
connectedCallback() {
|
|
101
99
|
const apiUrl = this.getAttribute("api-url");
|
|
102
|
-
if (apiUrl && !this._data)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this._render();
|
|
106
|
-
} else {
|
|
107
|
-
this._renderSkeleton();
|
|
108
|
-
}
|
|
100
|
+
if (apiUrl && !this._data) this._load();
|
|
101
|
+
else if (this._data) this._render();
|
|
102
|
+
else this._renderSkeleton();
|
|
109
103
|
}
|
|
110
|
-
attributeChangedCallback(name
|
|
104
|
+
attributeChangedCallback(name) {
|
|
111
105
|
if (name === "api-url") {
|
|
112
106
|
this._load();
|
|
113
107
|
return;
|
|
@@ -141,6 +135,12 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
141
135
|
get _label() {
|
|
142
136
|
return this.getAttribute("label") ?? "Contribution heatmap";
|
|
143
137
|
}
|
|
138
|
+
get _offsetX() {
|
|
139
|
+
return this._showDays ? DAY_LABEL_W : 0;
|
|
140
|
+
}
|
|
141
|
+
get _offsetY() {
|
|
142
|
+
return this._showMonths ? MONTH_LABEL_H + this._cellGap : 0;
|
|
143
|
+
}
|
|
144
144
|
async _load() {
|
|
145
145
|
const apiUrl = this.getAttribute("api-url");
|
|
146
146
|
const resolver = this._fetchData ?? (apiUrl ? () => fetch(apiUrl).then((r) => r.json()) : null);
|
|
@@ -154,133 +154,140 @@ var GitHeatmapElement = class extends HTMLElement {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
_renderSkeleton() {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;
|
|
158
|
+
const w = 53 * step + ox;
|
|
159
|
+
const h = 7 * step - cg + oy;
|
|
160
|
+
const svg = svgEl("svg", { width: w, height: h });
|
|
161
|
+
for (let wi = 0; wi < 53; wi++) {
|
|
162
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
163
|
+
svg.appendChild(svgEl("rect", { x: ox + wi * step, y: oy + dow * step, width: cs, height: cs, rx: cr, fill: "var(--ghm-color-l0)" }));
|
|
163
164
|
}
|
|
164
|
-
cols += `<div style="display:flex;flex-direction:column;gap:${gap}px;">${cells}</div>`;
|
|
165
165
|
}
|
|
166
|
-
this._shadow.innerHTML = `<style>${BASE_STYLES}</style
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;
|
|
167
|
+
const wrap = document.createElement("div");
|
|
168
|
+
wrap.style.opacity = "0.4";
|
|
169
|
+
wrap.appendChild(svg);
|
|
170
|
+
this._shadow.appendChild(wrap);
|
|
170
171
|
}
|
|
171
172
|
_renderError() {
|
|
172
|
-
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>
|
|
173
|
-
<p style="opacity:0.6;padding:1rem 0;">Could not load contribution data.</p>`;
|
|
173
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style><p style="opacity:0.6;padding:1rem 0;">Could not load contribution data.</p>`;
|
|
174
174
|
}
|
|
175
175
|
_render() {
|
|
176
176
|
if (!this._data) return;
|
|
177
177
|
const d = this._data;
|
|
178
|
-
const
|
|
178
|
+
const { _cellSize: cs, _cellGap: cg, _cellRadius: cr, _step: step, _offsetX: ox, _offsetY: oy } = this;
|
|
179
179
|
const levels = import_git_heatmap_core.DEFAULT_LEVELS;
|
|
180
|
-
const
|
|
181
|
-
|
|
180
|
+
const svgW = d.weeks.length * step + ox;
|
|
181
|
+
const svgH = 7 * step - cg + oy;
|
|
182
|
+
this._shadow.innerHTML = `<style>${BASE_STYLES}</style>`;
|
|
183
|
+
const wrapper = document.createElement("div");
|
|
184
|
+
wrapper.setAttribute("aria-label", this._label);
|
|
185
|
+
wrapper.style.position = "relative";
|
|
186
|
+
if (this._showTotal) {
|
|
187
|
+
const p = document.createElement("p");
|
|
188
|
+
p.style.cssText = "margin-bottom:12px; color:var(--ghm-text);";
|
|
189
|
+
p.textContent = `${d.totalContributions.toLocaleString()} contributions in the last year`;
|
|
190
|
+
wrapper.appendChild(p);
|
|
191
|
+
}
|
|
192
|
+
const scroll = document.createElement("div");
|
|
193
|
+
scroll.className = "ghm-scroll";
|
|
194
|
+
const svg = svgEl("svg", { width: svgW, height: svgH, role: "img", "aria-label": this._label });
|
|
182
195
|
if (this._showMonths) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
const labels = (0, import_git_heatmap_core.buildMonthLabels)(d.weeks);
|
|
197
|
+
for (const { label: ml, col } of labels) {
|
|
198
|
+
const t = svgEl("text", { x: ox + col * step, y: MONTH_LABEL_H - 4, fill: "var(--ghm-text)", "font-size": 10 });
|
|
199
|
+
t.textContent = ml;
|
|
200
|
+
svg.appendChild(t);
|
|
201
|
+
}
|
|
188
202
|
}
|
|
189
|
-
let dayLabelCol = "";
|
|
190
203
|
if (this._showDays) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
(
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
205
|
+
const lbl = import_git_heatmap_core.DAY_LABELS[dow];
|
|
206
|
+
if (!lbl) continue;
|
|
207
|
+
const t = svgEl("text", { x: DAY_LABEL_W - 6, y: oy + dow * step + cs, fill: "var(--ghm-text)", "font-size": 10, "text-anchor": "end" });
|
|
208
|
+
t.textContent = lbl;
|
|
209
|
+
svg.appendChild(t);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const mobileTip = document.createElement("div");
|
|
213
|
+
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;";
|
|
214
|
+
let activeTapDate = "";
|
|
215
|
+
const tt = document.createElement("div");
|
|
216
|
+
tt.className = "ghm-tooltip";
|
|
217
|
+
tt.style.display = "none";
|
|
218
|
+
for (let wi = 0; wi < d.weeks.length; wi++) {
|
|
219
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
220
|
+
const day = d.weeks[wi].days[dow];
|
|
221
|
+
if (!day?.date) continue;
|
|
222
|
+
const rect = svgEl("rect", {
|
|
223
|
+
x: ox + wi * step,
|
|
224
|
+
y: oy + dow * step,
|
|
225
|
+
width: cs,
|
|
226
|
+
height: cs,
|
|
227
|
+
rx: cr,
|
|
228
|
+
fill: `var(--ghm-color-l${day.level})`
|
|
229
|
+
});
|
|
230
|
+
rect.setAttribute("aria-label", this._tipText(day));
|
|
231
|
+
rect.setAttribute("role", "button");
|
|
232
|
+
rect.addEventListener("mouseenter", (e) => {
|
|
233
|
+
const r = rect.getBoundingClientRect();
|
|
234
|
+
const wr = wrapper.getBoundingClientRect();
|
|
235
|
+
tt.textContent = this._tipText(day);
|
|
236
|
+
tt.style.left = `${r.left - wr.left + cs / 2}px`;
|
|
237
|
+
tt.style.top = `${r.top - wr.top - 6}px`;
|
|
238
|
+
tt.style.display = "block";
|
|
239
|
+
});
|
|
240
|
+
rect.addEventListener("mouseleave", () => {
|
|
241
|
+
tt.style.display = "none";
|
|
242
|
+
});
|
|
243
|
+
rect.addEventListener("click", () => {
|
|
244
|
+
if (activeTapDate === day.date) {
|
|
245
|
+
activeTapDate = "";
|
|
246
|
+
mobileTip.style.display = "none";
|
|
247
|
+
} else {
|
|
248
|
+
activeTapDate = day.date;
|
|
249
|
+
mobileTip.textContent = this._tipText(day);
|
|
250
|
+
mobileTip.style.display = "block";
|
|
251
|
+
}
|
|
252
|
+
this.dispatchEvent(new CustomEvent("day-click", { detail: { date: day.date, count: day.count }, bubbles: true, composed: true }));
|
|
253
|
+
});
|
|
254
|
+
svg.appendChild(rect);
|
|
255
|
+
}
|
|
196
256
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return `<div
|
|
204
|
-
class="ghm-cell"
|
|
205
|
-
style="width:${cell}px;height:${cell}px;border-radius:${r}px;background:var(--ghm-color-l${lvl});"
|
|
206
|
-
data-date="${day.date}"
|
|
207
|
-
data-count="${day.count}"
|
|
208
|
-
aria-label="${tip}"
|
|
209
|
-
role="button"
|
|
210
|
-
tabindex="0"
|
|
211
|
-
></div>`;
|
|
212
|
-
}).join("");
|
|
213
|
-
return `<div style="display:flex;flex-direction:column;gap:${gap}px;">${cells}</div>`;
|
|
214
|
-
}).join("");
|
|
215
|
-
let legend = "";
|
|
257
|
+
scroll.appendChild(svg);
|
|
258
|
+
wrapper.appendChild(scroll);
|
|
259
|
+
requestAnimationFrame(() => {
|
|
260
|
+
scroll.scrollLeft = scroll.scrollWidth;
|
|
261
|
+
});
|
|
262
|
+
wrapper.appendChild(mobileTip);
|
|
216
263
|
if (this._showLegend) {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
264
|
+
const legend = document.createElement("div");
|
|
265
|
+
legend.style.cssText = "display:flex; align-items:center; justify-content:flex-end; margin-top:8px;";
|
|
266
|
+
const inner = document.createElement("div");
|
|
267
|
+
inner.style.cssText = "display:flex; align-items:center; gap:6px; font-size:0.9em;";
|
|
268
|
+
const less = document.createElement("span");
|
|
269
|
+
less.textContent = "Less";
|
|
270
|
+
const more = document.createElement("span");
|
|
271
|
+
more.textContent = "More";
|
|
272
|
+
inner.appendChild(less);
|
|
273
|
+
for (let i = 0; i < levels.length; i++) {
|
|
274
|
+
const s = svgEl("svg", { width: cs, height: cs });
|
|
275
|
+
const r = svgEl("rect", { width: cs, height: cs, rx: cr, fill: `var(--ghm-color-l${i})` });
|
|
276
|
+
const title = svgEl("title");
|
|
277
|
+
title.textContent = levels[i].label;
|
|
278
|
+
r.appendChild(title);
|
|
279
|
+
s.appendChild(r);
|
|
280
|
+
inner.appendChild(s);
|
|
281
|
+
}
|
|
282
|
+
inner.appendChild(more);
|
|
283
|
+
legend.appendChild(inner);
|
|
284
|
+
wrapper.appendChild(legend);
|
|
225
285
|
}
|
|
226
|
-
|
|
227
|
-
this._shadow.
|
|
228
|
-
<div aria-label="${this._label}" style="position:relative;">
|
|
229
|
-
${total}
|
|
230
|
-
<div class="ghm-scroll">
|
|
231
|
-
<div class="ghm-grid" style="min-width:${d.weeks.length * step}px;">
|
|
232
|
-
${monthRow}
|
|
233
|
-
<div style="display:flex;">
|
|
234
|
-
${dayLabelCol}
|
|
235
|
-
<div style="display:flex;gap:${gap}px;">${weekCols}</div>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
<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>
|
|
240
|
-
${legend}
|
|
241
|
-
<div class="ghm-tooltip" id="ghm-tt" style="display:none;"></div>
|
|
242
|
-
</div>`;
|
|
243
|
-
this._attachListeners();
|
|
244
|
-
const scrollEl = this._shadow.querySelector(".ghm-scroll");
|
|
245
|
-
if (scrollEl) scrollEl.scrollLeft = scrollEl.scrollWidth;
|
|
286
|
+
wrapper.appendChild(tt);
|
|
287
|
+
this._shadow.appendChild(wrapper);
|
|
246
288
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const tt = this._shadow.getElementById("ghm-tt");
|
|
250
|
-
const mobileTip = this._shadow.getElementById("ghm-mobile-tip");
|
|
251
|
-
let activeTapDate = "";
|
|
252
|
-
this._shadow.querySelectorAll(".ghm-cell").forEach((el) => {
|
|
253
|
-
const cell = el;
|
|
254
|
-
const date = cell.dataset["date"] ?? "";
|
|
255
|
-
const count = parseInt(cell.dataset["count"] ?? "0", 10);
|
|
256
|
-
const tip = count === 0 ? `No contributions on ${date}` : `${count} contribution${count > 1 ? "s" : ""} on ${date}`;
|
|
257
|
-
cell.addEventListener("mouseenter", (e) => {
|
|
258
|
-
const r = cell.getBoundingClientRect();
|
|
259
|
-
const wr = wrapper.getBoundingClientRect();
|
|
260
|
-
tt.textContent = tip;
|
|
261
|
-
tt.style.left = `${r.left - wr.left + this._cellSize / 2}px`;
|
|
262
|
-
tt.style.top = `${r.top - wr.top - 6}px`;
|
|
263
|
-
tt.style.display = "block";
|
|
264
|
-
});
|
|
265
|
-
cell.addEventListener("mouseleave", () => {
|
|
266
|
-
tt.style.display = "none";
|
|
267
|
-
});
|
|
268
|
-
cell.addEventListener("click", () => {
|
|
269
|
-
if (activeTapDate === date) {
|
|
270
|
-
activeTapDate = "";
|
|
271
|
-
mobileTip.style.display = "none";
|
|
272
|
-
} else {
|
|
273
|
-
activeTapDate = date;
|
|
274
|
-
mobileTip.textContent = tip;
|
|
275
|
-
mobileTip.style.display = "block";
|
|
276
|
-
}
|
|
277
|
-
this.dispatchEvent(new CustomEvent("day-click", {
|
|
278
|
-
detail: { date, count },
|
|
279
|
-
bubbles: true,
|
|
280
|
-
composed: true
|
|
281
|
-
}));
|
|
282
|
-
});
|
|
283
|
-
});
|
|
289
|
+
_tipText(day) {
|
|
290
|
+
return day.count === 0 ? `No contributions on ${day.date}` : `${day.count} contribution${day.count > 1 ? "s" : ""} on ${day.date}`;
|
|
284
291
|
}
|
|
285
292
|
};
|
|
286
293
|
if (typeof customElements !== "undefined" && !customElements.get("git-heatmap")) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/git-heatmap-element.ts"],"sourcesContent":["export { GitHeatmapElement } from \"./git-heatmap-element\";\n","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/index.ts","../src/git-heatmap-element.ts"],"sourcesContent":["export { GitHeatmapElement } from \"./git-heatmap-element\";\n","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":[]}
|