@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.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
- .ghm-grid { display: inline-flex; flex-direction: column; }
54
- .ghm-row { display: flex; }
55
- .ghm-col { display: flex; flex-direction: column; }
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
- this._load();
104
- } else if (this._data) {
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, _old, _val) {
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 cell = this._cellSize, gap = this._cellGap, step = this._step;
158
- let cols = "";
159
- for (let c = 0; c < 53; c++) {
160
- let cells = "";
161
- for (let r = 0; r < 7; r++) {
162
- cells += `<div style="width:${cell}px;height:${cell}px;border-radius:${this._cellRadius}px;background:var(--ghm-color-l0);flex-shrink:0;"></div>`;
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
- <div aria-label="${this._label}" style="opacity:0.4;">
168
- <div style="display:flex;gap:${gap}px;">${cols}</div>
169
- </div>`;
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 cell = this._cellSize, gap = this._cellGap, r = this._cellRadius, step = this._step;
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 monthLabels = this._showMonths ? (0, import_git_heatmap_core.buildMonthLabels)(d.weeks) : [];
181
- let monthRow = "";
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 cells = monthLabels.map(({ label, col }, idx) => {
184
- const nextCol = monthLabels[idx + 1]?.col ?? d.weeks.length;
185
- return `<div style="width:${(nextCol - col) * step}px;white-space:nowrap;font-size:0.9em;">${label}</div>`;
186
- }).join("");
187
- monthRow = `<div style="display:flex;margin-bottom:${gap}px;margin-left:${this._showDays ? 32 : 0}px;">${cells}</div>`;
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
- const rows = Array.from(
192
- { length: 7 },
193
- (_, dow) => `<div style="height:${cell}px;line-height:${cell}px;width:26px;text-align:right;padding-right:4px;font-size:0.9em;">${import_git_heatmap_core.DAY_LABELS[dow] ?? ""}</div>`
194
- ).join("");
195
- dayLabelCol = `<div style="display:flex;flex-direction:column;gap:${gap}px;margin-right:6px;">${rows}</div>`;
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
- const weekCols = d.weeks.map((week, _wi) => {
198
- const cells = Array.from({ length: 7 }, (_, dow) => {
199
- const day = week.days[dow];
200
- if (!day?.date) return `<div style="width:${cell}px;height:${cell}px;flex-shrink:0;"></div>`;
201
- const lvl = day.level;
202
- const tip = day.count === 0 ? `No contributions on ${day.date}` : `${day.count} contribution${day.count > 1 ? "s" : ""} on ${day.date}`;
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 cells = levels.map(
218
- (lvl, i) => `<div style="width:${cell}px;height:${cell}px;border-radius:${r}px;background:var(--ghm-color-l${i});" title="${lvl.label}"></div>`
219
- ).join("");
220
- legend = `<div style="display:flex;align-items:center;justify-content:flex-end;margin-top:8px;">
221
- <div style="display:flex;align-items:center;gap:6px;font-size:0.9em;">
222
- <span>Less</span>${cells}<span>More</span>
223
- </div>
224
- </div>`;
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
- const total = this._showTotal ? `<p style="margin-bottom:12px;color:var(--ghm-text);">${d.totalContributions.toLocaleString()} contributions in the last year</p>` : "";
227
- this._shadow.innerHTML = `<style>${BASE_STYLES}</style>
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
- _attachListeners() {
248
- const wrapper = this._shadow.firstElementChild;
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,8BAGO;AAGP,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkDb,IAAM,oBAAN,cAAgC,YAAY;AAAA,EAejD,cAAc;AACZ,UAAM;AAXR,SAAQ,QAA4B;AACpC,SAAQ,aAAkD;AAE1D,SAAQ,WAAkC;AASxC,SAAK,UAAU,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAAA,EACnD;AAAA,EAjBA,WAAW,qBAAqB;AAC9B,WAAO,CAAC,WAAW,aAAa,YAAY,eAAe,cAAc,eAAe,qBAAqB,mBAAmB,OAAO;AAAA,EACzI;AAAA,EAOA,IAAI,KAAK,OAAoB;AAAE,SAAK,QAAQ;AAAO,SAAK,QAAQ;AAAA,EAAG;AAAA,EACnE,IAAI,OAA2B;AAAE,WAAO,KAAK;AAAA,EAAO;AAAA,EAEpD,IAAI,UAAU,IAAgC;AAAE,SAAK,aAAa;AAAI,SAAK,MAAM;AAAA,EAAG;AAAA,EAOpF,oBAAoB;AAClB,UAAM,SAAS,KAAK,aAAa,SAAS;AAC1C,QAAI,UAAU,CAAC,KAAK,OAAO;AACzB,WAAK,MAAM;AAAA,IACb,WAAW,KAAK,OAAO;AACrB,WAAK,QAAQ;AAAA,IACf,OAAO;AACL,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,yBAAyB,MAAc,MAAqB,MAAqB;AAC/E,QAAI,SAAS,WAAW;AAAE,WAAK,MAAM;AAAG;AAAA,IAAQ;AAChD,QAAI,KAAK,MAAO,MAAK,QAAQ;AAAA,EAC/B;AAAA,EAEA,IAAY,YAAa;AAAE,WAAO,SAAS,KAAK,aAAa,WAAW,KAAM,OAAO,4BAAI,GAAG,EAAE;AAAA,EAAG;AAAA,EACjG,IAAY,WAAa;AAAE,WAAO,SAAS,KAAK,aAAa,UAAU,KAAO,OAAO,2BAAG,GAAI,EAAE;AAAA,EAAG;AAAA,EACjG,IAAY,cAAa;AAAE,WAAO,SAAS,KAAK,aAAa,aAAa,KAAK,KAAe,EAAE;AAAA,EAAG;AAAA,EACnG,IAAY,QAAa;AAAE,WAAO,KAAK,YAAY,KAAK;AAAA,EAAU;AAAA,EAClE,IAAY,aAAa;AAAE,WAAO,KAAK,aAAa,YAAY,MAAa;AAAA,EAAS;AAAA,EACtF,IAAY,cAAa;AAAE,WAAO,KAAK,aAAa,aAAa,MAAY;AAAA,EAAS;AAAA,EACtF,IAAY,cAAa;AAAE,WAAO,KAAK,aAAa,mBAAmB,MAAM;AAAA,EAAS;AAAA,EACtF,IAAY,YAAa;AAAE,WAAO,KAAK,aAAa,iBAAiB,MAAQ;AAAA,EAAS;AAAA,EACtF,IAAY,SAAa;AAAE,WAAO,KAAK,aAAa,OAAO,KAAK;AAAA,EAAwB;AAAA,EAExF,MAAc,QAAQ;AACpB,UAAM,SAAS,KAAK,aAAa,SAAS;AAC1C,UAAM,WAAW,KAAK,eAChB,SAAS,MAAM,MAAM,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI;AAC3D,QAAI,CAAC,SAAU;AAEf,SAAK,gBAAgB;AACrB,QAAI;AACF,WAAK,QAAQ,MAAM,SAAS;AAC5B,WAAK,QAAQ;AAAA,IACf,QAAQ;AACN,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA,EAEQ,kBAAkB;AACxB,UAAM,OAAO,KAAK,WAAW,MAAM,KAAK,UAAU,OAAO,KAAK;AAC9D,QAAI,OAAO;AACX,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAI,QAAQ;AACZ,eAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,iBAAS,qBAAqB,IAAI,aAAa,IAAI,oBAAoB,KAAK,WAAW;AAAA,MACzF;AACA,cAAQ,sDAAsD,GAAG,QAAQ,KAAK;AAAA,IAChF;AACA,SAAK,QAAQ,YAAY,UAAU,WAAW;AAAA,yBACzB,KAAK,MAAM;AAAA,uCACG,GAAG,QAAQ,IAAI;AAAA;AAAA,EAEpD;AAAA,EAEQ,eAAe;AACrB,SAAK,QAAQ,YAAY,UAAU,WAAW;AAAA;AAAA,EAEhD;AAAA,EAEQ,UAAU;AAChB,QAAI,CAAC,KAAK,MAAO;AACjB,UAAM,IAAI,KAAK;AACf,UAAM,OAAO,KAAK,WAAW,MAAM,KAAK,UAAU,IAAI,KAAK,aAAa,OAAO,KAAK;AACpF,UAAM,SAAwB;AAE9B,UAAM,cAAc,KAAK,kBAAc,0CAAiB,EAAE,KAAK,IAAI,CAAC;AAEpE,QAAI,WAAW;AACf,QAAI,KAAK,aAAa;AACpB,YAAM,QAAQ,YAAY,IAAI,CAAC,EAAE,OAAO,IAAI,GAAG,QAAQ;AACrD,cAAM,UAAU,YAAY,MAAM,CAAC,GAAG,OAAO,EAAE,MAAM;AACrD,eAAO,sBAAsB,UAAU,OAAO,IAAI,2CAA2C,KAAK;AAAA,MACpG,CAAC,EAAE,KAAK,EAAE;AACV,iBAAW,0CAA0C,GAAG,kBAAkB,KAAK,YAAY,KAAK,CAAC,QAAQ,KAAK;AAAA,IAChH;AAEA,QAAI,cAAc;AAClB,QAAI,KAAK,WAAW;AAClB,YAAM,OAAO,MAAM;AAAA,QAAK,EAAE,QAAQ,EAAE;AAAA,QAAG,CAAC,GAAG,QACzC,sBAAsB,IAAI,kBAAkB,IAAI,sEAAsE,mCAAW,GAAG,KAAK,EAAE;AAAA,MAC7I,EAAE,KAAK,EAAE;AACT,oBAAc,sDAAsD,GAAG,yBAAyB,IAAI;AAAA,IACtG;AAEA,UAAM,WAAW,EAAE,MAAM,IAAI,CAAC,MAAM,QAAQ;AAC1C,YAAM,QAAQ,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,QAAQ;AAClD,cAAM,MAAM,KAAK,KAAK,GAAG;AACzB,YAAI,CAAC,KAAK,KAAM,QAAO,qBAAqB,IAAI,aAAa,IAAI;AACjE,cAAM,MAAM,IAAI;AAChB,cAAM,MAAM,IAAI,UAAU,IACtB,uBAAuB,IAAI,IAAI,KAC/B,GAAG,IAAI,KAAK,gBAAgB,IAAI,QAAQ,IAAI,MAAM,EAAE,OAAO,IAAI,IAAI;AACvE,eAAO;AAAA;AAAA,yBAEU,IAAI,aAAa,IAAI,oBAAoB,CAAC,kCAAkC,GAAG;AAAA,uBACjF,IAAI,IAAI;AAAA,wBACP,IAAI,KAAK;AAAA,wBACT,GAAG;AAAA;AAAA;AAAA;AAAA,MAIrB,CAAC,EAAE,KAAK,EAAE;AACV,aAAO,sDAAsD,GAAG,QAAQ,KAAK;AAAA,IAC/E,CAAC,EAAE,KAAK,EAAE;AAEV,QAAI,SAAS;AACb,QAAI,KAAK,aAAa;AACpB,YAAM,QAAQ,OAAO;AAAA,QAAI,CAAC,KAAK,MAC7B,qBAAqB,IAAI,aAAa,IAAI,oBAAoB,CAAC,kCAAkC,CAAC,cAAc,IAAI,KAAK;AAAA,MAC3H,EAAE,KAAK,EAAE;AACT,eAAS;AAAA;AAAA,6BAEc,KAAK;AAAA;AAAA;AAAA,IAG9B;AAEA,UAAM,QAAQ,KAAK,aACf,wDAAwD,EAAE,mBAAmB,eAAe,CAAC,wCAC7F;AAEJ,SAAK,QAAQ,YAAY,UAAU,WAAW;AAAA,yBACzB,KAAK,MAAM;AAAA,UAC1B,KAAK;AAAA;AAAA,mDAEoC,EAAE,MAAM,SAAS,IAAI;AAAA,cAC1D,QAAQ;AAAA;AAAA,gBAEN,WAAW;AAAA,6CACkB,GAAG,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,UAKtD,MAAM;AAAA;AAAA;AAIZ,SAAK,iBAAiB;AAGtB,UAAM,WAAW,KAAK,QAAQ,cAAc,aAAa;AACzD,QAAI,SAAU,UAAS,aAAa,SAAS;AAAA,EAC/C;AAAA,EAEQ,mBAAmB;AACzB,UAAM,UAAU,KAAK,QAAQ;AAC7B,UAAM,KAAK,KAAK,QAAQ,eAAe,QAAQ;AAC/C,UAAM,YAAY,KAAK,QAAQ,eAAe,gBAAgB;AAC9D,QAAI,gBAAgB;AAEpB,SAAK,QAAQ,iBAAiB,WAAW,EAAE,QAAQ,CAAC,OAAO;AACzD,YAAM,OAAO;AACb,YAAM,OAAQ,KAAK,QAAQ,MAAM,KAAM;AACvC,YAAM,QAAQ,SAAS,KAAK,QAAQ,OAAO,KAAK,KAAK,EAAE;AACvD,YAAM,MAAM,UAAU,IAClB,uBAAuB,IAAI,KAC3B,GAAG,KAAK,gBAAgB,QAAQ,IAAI,MAAM,EAAE,OAAO,IAAI;AAE3D,WAAK,iBAAiB,cAAc,CAAC,MAAM;AACzC,cAAM,IAAK,KAAK,sBAAsB;AACtC,cAAM,KAAK,QAAQ,sBAAsB;AACzC,WAAG,cAAc;AACjB,WAAG,MAAM,OAAQ,GAAG,EAAE,OAAO,GAAG,OAAO,KAAK,YAAY,CAAC;AACzD,WAAG,MAAM,MAAQ,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;AACtC,WAAG,MAAM,UAAU;AAAA,MACrB,CAAC;AACD,WAAK,iBAAiB,cAAc,MAAM;AAAE,WAAG,MAAM,UAAU;AAAA,MAAQ,CAAC;AACxE,WAAK,iBAAiB,SAAS,MAAM;AACnC,YAAI,kBAAkB,MAAM;AAC1B,0BAAgB;AAChB,oBAAU,MAAM,UAAU;AAAA,QAC5B,OAAO;AACL,0BAAgB;AAChB,oBAAU,cAAc;AACxB,oBAAU,MAAM,UAAU;AAAA,QAC5B;AACA,aAAK,cAAc,IAAI,YAAY,aAAa;AAAA,UAC9C,QAAQ,EAAE,MAAM,MAAM;AAAA,UACtB,SAAS;AAAA,UACT,UAAU;AAAA,QACZ,CAAC,CAAC;AAAA,MACJ,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;AAEA,IAAI,OAAO,mBAAmB,eAAe,CAAC,eAAe,IAAI,aAAa,GAAG;AAC/E,iBAAe,OAAO,eAAe,iBAAiB;AACxD;","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":[]}