@rsalianto/git-heatmap-angular 0.1.0 → 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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @rsalianto/git-heatmap-angular
2
+
3
+ Angular standalone component for displaying GitHub/GitLab contribution heatmaps.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rsalianto/git-heatmap-angular @rsalianto/git-heatmap-core
9
+ ```
10
+
11
+ Requires Angular 16+.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Component } from "@angular/core";
17
+ import { GitHeatmapComponent } from "@rsalianto/git-heatmap-angular";
18
+
19
+ @Component({
20
+ standalone: true,
21
+ imports: [GitHeatmapComponent],
22
+ template: `<git-heatmap apiUrl="/api/contributions" />`,
23
+ })
24
+ export class MyComponent {}
25
+ ```
26
+
27
+ ### Pass data directly
28
+
29
+ ```ts
30
+ import { normalizeManual } from "@rsalianto/git-heatmap-core";
31
+
32
+ @Component({
33
+ standalone: true,
34
+ imports: [GitHeatmapComponent],
35
+ template: `<git-heatmap [data]="data" />`,
36
+ })
37
+ export class MyComponent {
38
+ data = normalizeManual([
39
+ { date: "2024-06-01", count: 4 },
40
+ { date: "2024-06-02", count: 0 },
41
+ ]);
42
+ }
43
+ ```
44
+
45
+ ## Inputs
46
+
47
+ | Input | Type | Default | Description |
48
+ |---|---|---|---|
49
+ | `data` | `HeatmapData` | — | Static data (skips fetching) |
50
+ | `apiUrl` | `string` | — | Endpoint returning `HeatmapData` JSON |
51
+ | `fetchData` | `() => Promise<HeatmapData>` | — | Custom fetch function |
52
+ | `levels` | `LevelConfig[]` | `DEFAULT_LEVELS` | Color thresholds |
53
+ | `cellSize` | `number` | `10` | Cell size in px |
54
+ | `cellGap` | `number` | `3` | Gap between cells in px |
55
+ | `cellRadius` | `number` | `2` | Cell border radius in px |
56
+ | `showTotal` | `boolean` | `true` | Show total contributions |
57
+ | `showLegend` | `boolean` | `true` | Show Less/More legend |
58
+ | `showMonthLabels` | `boolean` | `true` | Show month labels |
59
+ | `showDayLabels` | `boolean` | `true` | Show Mon/Wed/Fri labels |
60
+ | `theme` | `Partial<HeatmapTheme>` | — | Override theme values |
61
+ | `label` | `string` | `"Contribution heatmap"` | Accessible label |
62
+
63
+ ## Outputs
64
+
65
+ | Output | Payload | Description |
66
+ |---|---|---|
67
+ | `dayClick` | `HeatmapDay` | Emitted when a cell is clicked |
68
+
69
+ ```ts
70
+ <git-heatmap apiUrl="/api/contributions" (dayClick)="onDayClick($event)" />
71
+ ```
72
+
73
+ ## Theming
74
+
75
+ Override via `theme` input or CSS variables on the host element:
76
+
77
+ ```css
78
+ git-heatmap {
79
+ --ghm-color-l0: #161b22;
80
+ --ghm-color-l1: #0e4429;
81
+ --ghm-color-l2: #006d32;
82
+ --ghm-color-l3: #26a641;
83
+ --ghm-color-l4: #39d353;
84
+ }
85
+ ```
86
+
87
+ ## Related packages
88
+
89
+ - [`@rsalianto/git-heatmap-core`](https://www.npmjs.com/package/@rsalianto/git-heatmap-core) — Core utilities
90
+ - [`@rsalianto/git-heatmap-react`](https://www.npmjs.com/package/@rsalianto/git-heatmap-react) — React component
package/dist/index.d.mts CHANGED
@@ -1,6 +1,5 @@
1
- import * as _rsalianto_git_heatmap_core from '@rsalianto/git-heatmap-core';
2
- import { HeatmapData, LevelConfig, HeatmapTheme, HeatmapDay } from '@rsalianto/git-heatmap-core';
3
- import { OnInit, OnChanges, EventEmitter, SimpleChanges } from '@angular/core';
1
+ import { OnInit, OnChanges, EventEmitter } from '@angular/core';
2
+ import { HeatmapData, LevelConfig, HeatmapTheme, HeatmapDay, MonthLabel } from '@rsalianto/git-heatmap-core';
4
3
 
5
4
  declare class GitHeatmapComponent implements OnInit, OnChanges {
6
5
  data?: HeatmapData;
@@ -26,17 +25,23 @@ declare class GitHeatmapComponent implements OnInit, OnChanges {
26
25
  x: number;
27
26
  y: number;
28
27
  } | null;
28
+ readonly dayLabelW = 32;
29
+ readonly monthLabelH = 16;
30
+ readonly dayLabels: Record<number, string>;
31
+ readonly skeletonCols: number[];
32
+ readonly skeletonRows: number[];
29
33
  get step(): number;
30
- get skeletonCols(): unknown[];
31
- get skeletonRows(): unknown[];
32
- get dayLabels(): Record<number, string>;
33
- get cssVars(): Record<string, string>;
34
- get monthLabels(): _rsalianto_git_heatmap_core.MonthLabel[];
34
+ get offsetX(): 32 | 0;
35
+ get offsetY(): number;
36
+ get svgHeight(): number;
37
+ svgWidth(weeks: number): number;
38
+ get cssVars(): string;
39
+ get monthLabels(): MonthLabel[];
35
40
  private cdr;
36
41
  private el;
37
42
  private renderer;
38
43
  ngOnInit(): void;
39
- ngOnChanges(changes: SimpleChanges): void;
44
+ ngOnChanges(): void;
40
45
  private load;
41
46
  formatTooltip(day: HeatmapDay): string;
42
47
  onMouseEnter(e: MouseEvent, day: HeatmapDay): void;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import * as _rsalianto_git_heatmap_core from '@rsalianto/git-heatmap-core';
2
- import { HeatmapData, LevelConfig, HeatmapTheme, HeatmapDay } from '@rsalianto/git-heatmap-core';
3
- import { OnInit, OnChanges, EventEmitter, SimpleChanges } from '@angular/core';
1
+ import { OnInit, OnChanges, EventEmitter } from '@angular/core';
2
+ import { HeatmapData, LevelConfig, HeatmapTheme, HeatmapDay, MonthLabel } from '@rsalianto/git-heatmap-core';
4
3
 
5
4
  declare class GitHeatmapComponent implements OnInit, OnChanges {
6
5
  data?: HeatmapData;
@@ -26,17 +25,23 @@ declare class GitHeatmapComponent implements OnInit, OnChanges {
26
25
  x: number;
27
26
  y: number;
28
27
  } | null;
28
+ readonly dayLabelW = 32;
29
+ readonly monthLabelH = 16;
30
+ readonly dayLabels: Record<number, string>;
31
+ readonly skeletonCols: number[];
32
+ readonly skeletonRows: number[];
29
33
  get step(): number;
30
- get skeletonCols(): unknown[];
31
- get skeletonRows(): unknown[];
32
- get dayLabels(): Record<number, string>;
33
- get cssVars(): Record<string, string>;
34
- get monthLabels(): _rsalianto_git_heatmap_core.MonthLabel[];
34
+ get offsetX(): 32 | 0;
35
+ get offsetY(): number;
36
+ get svgHeight(): number;
37
+ svgWidth(weeks: number): number;
38
+ get cssVars(): string;
39
+ get monthLabels(): MonthLabel[];
35
40
  private cdr;
36
41
  private el;
37
42
  private renderer;
38
43
  ngOnInit(): void;
39
- ngOnChanges(changes: SimpleChanges): void;
44
+ ngOnChanges(): void;
40
45
  private load;
41
46
  formatTooltip(day: HeatmapDay): string;
42
47
  onMouseEnter(e: MouseEvent, day: HeatmapDay): void;
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ module.exports = __toCommonJS(index_exports);
36
36
  var import_core = require("@angular/core");
37
37
  var import_common = require("@angular/common");
38
38
  var import_git_heatmap_core = require("@rsalianto/git-heatmap-core");
39
+ var DAY_LABEL_W = 32;
40
+ var MONTH_LABEL_H = 16;
39
41
  var GitHeatmapComponent = class {
40
42
  constructor() {
41
43
  this.levels = import_git_heatmap_core.DEFAULT_LEVELS;
@@ -54,6 +56,11 @@ var GitHeatmapComponent = class {
54
56
  this.error = null;
55
57
  this.tappedDay = null;
56
58
  this.tooltip = null;
59
+ this.dayLabelW = DAY_LABEL_W;
60
+ this.monthLabelH = MONTH_LABEL_H;
61
+ this.dayLabels = import_git_heatmap_core.DAY_LABELS;
62
+ this.skeletonCols = Array.from({ length: 53 }, (_, i) => i);
63
+ this.skeletonRows = Array.from({ length: 7 }, (_, i) => i);
57
64
  this.cdr = (0, import_core.inject)(import_core.ChangeDetectorRef);
58
65
  this.el = (0, import_core.inject)(import_core.ElementRef);
59
66
  this.renderer = (0, import_core.inject)(import_core.Renderer2);
@@ -61,30 +68,33 @@ var GitHeatmapComponent = class {
61
68
  get step() {
62
69
  return this.cellSize + this.cellGap;
63
70
  }
64
- get skeletonCols() {
65
- return Array.from({ length: 53 });
71
+ get offsetX() {
72
+ return this.showDayLabels ? DAY_LABEL_W : 0;
66
73
  }
67
- get skeletonRows() {
68
- return Array.from({ length: 7 });
74
+ get offsetY() {
75
+ return this.showMonthLabels ? MONTH_LABEL_H + this.cellGap : 0;
69
76
  }
70
- get dayLabels() {
71
- return import_git_heatmap_core.DAY_LABELS;
77
+ get svgHeight() {
78
+ return 7 * this.step - this.cellGap + this.offsetY;
79
+ }
80
+ svgWidth(weeks) {
81
+ return weeks * this.step + this.offsetX;
72
82
  }
73
83
  get cssVars() {
74
84
  const t = { ...import_git_heatmap_core.DEFAULT_THEME, ...this.theme };
75
- return {
76
- "--ghm-color-l0": t.colorL0,
77
- "--ghm-color-l1": t.colorL1,
78
- "--ghm-color-l2": t.colorL2,
79
- "--ghm-color-l3": t.colorL3,
80
- "--ghm-color-l4": t.colorL4,
81
- "--ghm-text": t.textColor,
82
- "--ghm-tooltip-bg": t.tooltipBg,
83
- "--ghm-tooltip-border": t.tooltipBorderColor,
84
- "--ghm-tooltip-text": t.tooltipTextColor,
85
- "--ghm-font": t.fontFamily,
86
- "--ghm-fs": t.fontSize
87
- };
85
+ return [
86
+ `--ghm-color-l0:${t.colorL0}`,
87
+ `--ghm-color-l1:${t.colorL1}`,
88
+ `--ghm-color-l2:${t.colorL2}`,
89
+ `--ghm-color-l3:${t.colorL3}`,
90
+ `--ghm-color-l4:${t.colorL4}`,
91
+ `--ghm-text:${t.textColor}`,
92
+ `--ghm-tooltip-bg:${t.tooltipBg}`,
93
+ `--ghm-tooltip-border:${t.tooltipBorderColor}`,
94
+ `--ghm-tooltip-text:${t.tooltipTextColor}`,
95
+ `--ghm-font:${t.fontFamily}`,
96
+ `--ghm-fs:${t.fontSize}`
97
+ ].join(";");
88
98
  }
89
99
  get monthLabels() {
90
100
  return this.showMonthLabels && this.calendarData ? (0, import_git_heatmap_core.buildMonthLabels)(this.calendarData.weeks) : [];
@@ -93,8 +103,8 @@ var GitHeatmapComponent = class {
93
103
  this.injectStyles();
94
104
  this.load();
95
105
  }
96
- ngOnChanges(changes) {
97
- if (changes["apiUrl"] || changes["data"]) this.load();
106
+ ngOnChanges() {
107
+ this.load();
98
108
  }
99
109
  async load() {
100
110
  if (this.data) {
@@ -120,8 +130,7 @@ var GitHeatmapComponent = class {
120
130
  return day.count === 0 ? `No contributions on ${day.date}` : `${day.count} contribution${day.count > 1 ? "s" : ""} on ${day.date}`;
121
131
  }
122
132
  onMouseEnter(e, day) {
123
- const target = e.currentTarget;
124
- const r = target.getBoundingClientRect();
133
+ const r = e.currentTarget.getBoundingClientRect();
125
134
  const wr = this.el.nativeElement.getBoundingClientRect();
126
135
  this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };
127
136
  this.cdr.markForCheck();
@@ -132,8 +141,7 @@ var GitHeatmapComponent = class {
132
141
  this.cdr.markForCheck();
133
142
  }
134
143
  injectStyles() {
135
- if (typeof document === "undefined") return;
136
- if (document.getElementById("ghm-style")) return;
144
+ if (typeof document === "undefined" || document.getElementById("ghm-style")) return;
137
145
  const style = this.renderer.createElement("style");
138
146
  style.id = "ghm-style";
139
147
  style.textContent = `
@@ -143,6 +151,8 @@ var GitHeatmapComponent = class {
143
151
  color:var(--ghm-text); user-select:none; box-sizing:border-box;
144
152
  }
145
153
  [data-git-heatmap] * { box-sizing:border-box; }
154
+ [data-git-heatmap] rect { transition:opacity 0.15s; cursor:pointer; }
155
+ [data-git-heatmap] rect:hover { opacity:0.7; }
146
156
  `;
147
157
  this.renderer.appendChild(document.head, style);
148
158
  }
@@ -193,93 +203,103 @@ GitHeatmapComponent = __decorateClass([
193
203
  (0, import_core.Component)({
194
204
  selector: "git-heatmap",
195
205
  standalone: true,
196
- imports: [import_common.NgFor, import_common.NgIf, import_common.NgStyle],
206
+ imports: [import_common.NgFor, import_common.NgIf],
197
207
  encapsulation: import_core.ViewEncapsulation.None,
198
208
  changeDetection: import_core.ChangeDetectionStrategy.OnPush,
199
209
  template: `
200
- <div class="ghm-wrapper" [attr.data-git-heatmap]="''" [ngStyle]="cssVars" [attr.aria-label]="label">
210
+ <div class="ghm-wrapper" [attr.data-git-heatmap]="''" [style]="cssVars" [attr.aria-label]="label" style="position:relative;">
201
211
 
202
212
  <ng-container *ngIf="error">
203
213
  <p style="opacity:0.6; padding:1rem 0;">Could not load contribution data.</p>
204
214
  </ng-container>
205
215
 
216
+ <!-- Skeleton -->
206
217
  <ng-container *ngIf="!error && (status === 'loading' || status === 'idle')">
207
- <div *ngIf="showTotal" style="height:16px;width:160px;background:var(--ghm-text);border-radius:4px;margin-bottom:12px;opacity:0.2;"></div>
208
- <div [ngStyle]="{ display:'flex', gap: cellGap + 'px', opacity: 0.4 }">
209
- <div *ngFor="let _ of skeletonCols" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }">
210
- <div *ngFor="let __ of skeletonRows" [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l0)' }"></div>
211
- </div>
212
- </div>
218
+ <div *ngIf="showTotal" style="height:14px; width:160px; background:var(--ghm-text); border-radius:4px; margin-bottom:12px; opacity:0.2;"></div>
219
+ <svg [attr.width]="svgWidth(53)" [attr.height]="svgHeight" style="display:block; opacity:0.4;">
220
+ <ng-container *ngFor="let wi of skeletonCols">
221
+ <rect
222
+ *ngFor="let dow of skeletonRows"
223
+ [attr.x]="offsetX + wi * step" [attr.y]="offsetY + dow * step"
224
+ [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius"
225
+ fill="var(--ghm-color-l0)"
226
+ />
227
+ </ng-container>
228
+ </svg>
213
229
  </ng-container>
214
230
 
231
+ <!-- Loaded -->
215
232
  <ng-container *ngIf="!error && status === 'success' && calendarData">
216
- <p *ngIf="showTotal" style="margin-bottom:12px;color:var(--ghm-text);">
233
+ <p *ngIf="showTotal" style="margin-bottom:12px; color:var(--ghm-text);">
217
234
  {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year
218
235
  </p>
219
236
 
220
- <div #scrollRef style="overflow-x:auto;overflow-y:visible;padding-bottom:4px;">
221
- <div [ngStyle]="{ display:'inline-flex', flexDirection:'column', minWidth: (calendarData.weeks.length * step) + 'px' }">
222
-
223
- <div *ngIf="showMonthLabels" [ngStyle]="{ display:'flex', marginBottom: cellGap + 'px', marginLeft: showDayLabels ? '32px' : '0' }">
224
- <div *ngFor="let ml of monthLabels; let idx = index"
225
- [ngStyle]="{ width: ((monthLabels[idx+1]?.col ?? calendarData.weeks.length) - ml.col) * step + 'px', whiteSpace:'nowrap', fontSize:'0.9em' }">
226
- {{ ml.label }}
227
- </div>
228
- </div>
237
+ <div #scrollRef style="overflow-x:auto; overflow-y:visible; padding-bottom:4px;">
238
+ <svg
239
+ [attr.width]="svgWidth(calendarData.weeks.length)"
240
+ [attr.height]="svgHeight"
241
+ style="display:block; overflow:visible;"
242
+ role="img" [attr.aria-label]="label"
243
+ >
244
+ <!-- Month labels -->
245
+ <text
246
+ *ngFor="let ml of monthLabels"
247
+ [attr.x]="offsetX + ml.col * step"
248
+ [attr.y]="monthLabelH - 4"
249
+ fill="var(--ghm-text)" font-size="10"
250
+ >{{ ml.label }}</text>
229
251
 
230
- <div style="display:flex;">
231
- <div *ngIf="showDayLabels" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px', marginRight:'6px' }">
232
- <div *ngFor="let dow of [0,1,2,3,4,5,6]"
233
- [ngStyle]="{ height: cellSize + 'px', lineHeight: cellSize + 'px', width:'26px', textAlign:'right', paddingRight:'4px', fontSize:'0.9em' }">
234
- {{ dayLabels[dow] || '' }}
235
- </div>
236
- </div>
252
+ <!-- Day labels -->
253
+ <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
254
+ <text
255
+ *ngIf="dayLabels[dow]"
256
+ [attr.x]="dayLabelW - 6"
257
+ [attr.y]="offsetY + dow * step + cellSize"
258
+ fill="var(--ghm-text)" font-size="10" text-anchor="end"
259
+ >{{ dayLabels[dow] }}</text>
260
+ </ng-container>
237
261
 
238
- <div [ngStyle]="{ display:'flex', gap: cellGap + 'px' }">
239
- <div *ngFor="let week of calendarData.weeks; let wi = index" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }">
240
- <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
241
- <div *ngIf="week.days[dow]?.date; else emptyCell"
242
- [ngStyle]="{
243
- width: cellSize + 'px', height: cellSize + 'px',
244
- borderRadius: cellRadius + 'px',
245
- background: 'var(--ghm-color-l' + week.days[dow].level + ')',
246
- cursor: 'pointer', transition: 'opacity 0.15s'
247
- }"
248
- [attr.aria-label]="formatTooltip(week.days[dow])"
249
- role="button"
250
- tabindex="0"
251
- (mouseenter)="onMouseEnter($event, week.days[dow])"
252
- (mouseleave)="tooltip = null; cdr.markForCheck()"
253
- (mouseover)="$any($event.currentTarget).style.opacity='0.7'"
254
- (mouseout)="$any($event.currentTarget).style.opacity='1'"
255
- (click)="onCellClick(week.days[dow])">
256
- </div>
257
- <ng-template #emptyCell>
258
- <div [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px' }"></div>
259
- </ng-template>
260
- </ng-container>
261
- </div>
262
- </div>
263
- </div>
264
- </div>
262
+ <!-- Grid -->
263
+ <ng-container *ngFor="let week of calendarData.weeks; let wi = index">
264
+ <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
265
+ <rect
266
+ *ngIf="week.days[dow]?.date"
267
+ [attr.x]="offsetX + wi * step"
268
+ [attr.y]="offsetY + dow * step"
269
+ [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius"
270
+ [attr.fill]="'var(--ghm-color-l' + week.days[dow].level + ')'"
271
+ style="cursor:pointer;"
272
+ [attr.aria-label]="formatTooltip(week.days[dow])"
273
+ role="button"
274
+ (mouseenter)="onMouseEnter($event, week.days[dow])"
275
+ (mouseleave)="tooltip = null; cdr.markForCheck()"
276
+ (click)="onCellClick(week.days[dow])"
277
+ />
278
+ </ng-container>
279
+ </ng-container>
280
+ </svg>
265
281
  </div>
266
282
 
267
- <div *ngIf="tappedDay" style="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;">
283
+ <!-- Mobile tapped info -->
284
+ <div *ngIf="tappedDay" style="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;">
268
285
  {{ formatTooltip(tappedDay) }}
269
286
  </div>
270
287
 
271
- <div *ngIf="showLegend" style="display:flex;align-items:center;justify-content:flex-end;margin-top:8px;">
272
- <div style="display:flex;align-items:center;gap:6px;font-size:0.9em;">
288
+ <!-- Legend -->
289
+ <div *ngIf="showLegend" style="display:flex; align-items:center; justify-content:flex-end; margin-top:8px;">
290
+ <div style="display:flex; align-items:center; gap:6px; font-size:0.9em;">
273
291
  <span>Less</span>
274
- <div *ngFor="let lvl of levels; let i = index"
275
- [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l' + i + ')' }"
276
- [attr.title]="lvl.label">
277
- </div>
292
+ <svg *ngFor="let lvl of levels; let i = index" [attr.width]="cellSize" [attr.height]="cellSize" style="display:block;">
293
+ <rect [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius" [attr.fill]="'var(--ghm-color-l' + i + ')'">
294
+ <title>{{ lvl.label }}</title>
295
+ </rect>
296
+ </svg>
278
297
  <span>More</span>
279
298
  </div>
280
299
  </div>
281
300
 
282
- <div *ngIf="tooltip" [ngStyle]="{
301
+ <!-- Tooltip -->
302
+ <div *ngIf="tooltip" [style]="{
283
303
  pointerEvents:'none', position:'absolute', zIndex:50,
284
304
  left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',
285
305
  transform:'translate(-50%,-100%)',
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/lib/git-heatmap.component.ts"],"sourcesContent":["export { GitHeatmapComponent } from \"./lib/git-heatmap.component\";\n","import {\n Component, Input, Output, EventEmitter,\n OnInit, OnChanges, SimpleChanges,\n ChangeDetectionStrategy, ChangeDetectorRef,\n ViewEncapsulation, ElementRef, Renderer2,\n inject,\n} from \"@angular/core\";\nimport { NgFor, NgIf, NgStyle } from \"@angular/common\";\nimport {\n buildMonthLabels, CELL, GAP, STEP, DAY_LABELS,\n DEFAULT_LEVELS, DEFAULT_THEME,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, HeatmapTheme, LevelConfig } from \"@rsalianto/git-heatmap-core\";\n\n@Component({\n selector: \"git-heatmap\",\n standalone: true,\n imports: [NgFor, NgIf, NgStyle],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"ghm-wrapper\" [attr.data-git-heatmap]=\"''\" [ngStyle]=\"cssVars\" [attr.aria-label]=\"label\">\n\n <ng-container *ngIf=\"error\">\n <p style=\"opacity:0.6; padding:1rem 0;\">Could not load contribution data.</p>\n </ng-container>\n\n <ng-container *ngIf=\"!error && (status === 'loading' || status === 'idle')\">\n <div *ngIf=\"showTotal\" style=\"height:16px;width:160px;background:var(--ghm-text);border-radius:4px;margin-bottom:12px;opacity:0.2;\"></div>\n <div [ngStyle]=\"{ display:'flex', gap: cellGap + 'px', opacity: 0.4 }\">\n <div *ngFor=\"let _ of skeletonCols\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }\">\n <div *ngFor=\"let __ of skeletonRows\" [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l0)' }\"></div>\n </div>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!error && status === 'success' && calendarData\">\n <p *ngIf=\"showTotal\" style=\"margin-bottom:12px;color:var(--ghm-text);\">\n {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year\n </p>\n\n <div #scrollRef style=\"overflow-x:auto;overflow-y:visible;padding-bottom:4px;\">\n <div [ngStyle]=\"{ display:'inline-flex', flexDirection:'column', minWidth: (calendarData.weeks.length * step) + 'px' }\">\n\n <div *ngIf=\"showMonthLabels\" [ngStyle]=\"{ display:'flex', marginBottom: cellGap + 'px', marginLeft: showDayLabels ? '32px' : '0' }\">\n <div *ngFor=\"let ml of monthLabels; let idx = index\"\n [ngStyle]=\"{ width: ((monthLabels[idx+1]?.col ?? calendarData.weeks.length) - ml.col) * step + 'px', whiteSpace:'nowrap', fontSize:'0.9em' }\">\n {{ ml.label }}\n </div>\n </div>\n\n <div style=\"display:flex;\">\n <div *ngIf=\"showDayLabels\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px', marginRight:'6px' }\">\n <div *ngFor=\"let dow of [0,1,2,3,4,5,6]\"\n [ngStyle]=\"{ height: cellSize + 'px', lineHeight: cellSize + 'px', width:'26px', textAlign:'right', paddingRight:'4px', fontSize:'0.9em' }\">\n {{ dayLabels[dow] || '' }}\n </div>\n </div>\n\n <div [ngStyle]=\"{ display:'flex', gap: cellGap + 'px' }\">\n <div *ngFor=\"let week of calendarData.weeks; let wi = index\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }\">\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <div *ngIf=\"week.days[dow]?.date; else emptyCell\"\n [ngStyle]=\"{\n width: cellSize + 'px', height: cellSize + 'px',\n borderRadius: cellRadius + 'px',\n background: 'var(--ghm-color-l' + week.days[dow].level + ')',\n cursor: 'pointer', transition: 'opacity 0.15s'\n }\"\n [attr.aria-label]=\"formatTooltip(week.days[dow])\"\n role=\"button\"\n tabindex=\"0\"\n (mouseenter)=\"onMouseEnter($event, week.days[dow])\"\n (mouseleave)=\"tooltip = null; cdr.markForCheck()\"\n (mouseover)=\"$any($event.currentTarget).style.opacity='0.7'\"\n (mouseout)=\"$any($event.currentTarget).style.opacity='1'\"\n (click)=\"onCellClick(week.days[dow])\">\n </div>\n <ng-template #emptyCell>\n <div [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px' }\"></div>\n </ng-template>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div *ngIf=\"tappedDay\" style=\"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 {{ formatTooltip(tappedDay) }}\n </div>\n\n <div *ngIf=\"showLegend\" 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>\n <div *ngFor=\"let lvl of levels; let i = index\"\n [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l' + i + ')' }\"\n [attr.title]=\"lvl.label\">\n </div>\n <span>More</span>\n </div>\n </div>\n\n <div *ngIf=\"tooltip\" [ngStyle]=\"{\n pointerEvents:'none', position:'absolute', zIndex:50,\n left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',\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 fontSize:'0.9em', borderRadius:'4px', padding:'3px 8px', whiteSpace:'nowrap'\n }\">\n {{ formatTooltip(tooltip.day) }}\n </div>\n </ng-container>\n\n </div>\n `,\n})\nexport class GitHeatmapComponent implements OnInit, OnChanges {\n @Input() data?: HeatmapData;\n @Input() apiUrl?: string;\n @Input() fetchData?: () => Promise<HeatmapData>;\n @Input() levels: LevelConfig[] = DEFAULT_LEVELS;\n @Input() cellSize = CELL;\n @Input() cellGap = GAP;\n @Input() cellRadius = 2;\n @Input() showTotal = true;\n @Input() showLegend = true;\n @Input() showMonthLabels = true;\n @Input() showDayLabels = true;\n @Input() theme: Partial<HeatmapTheme> = {};\n @Input() label = \"Contribution heatmap\";\n @Output() dayClick = new EventEmitter<HeatmapDay>();\n\n calendarData: HeatmapData | null = null;\n status: \"idle\" | \"loading\" | \"success\" | \"error\" = \"idle\";\n error: Error | null = null;\n tappedDay: HeatmapDay | null = null;\n tooltip: { day: HeatmapDay; x: number; y: number } | null = null;\n\n get step() { return this.cellSize + this.cellGap; }\n get skeletonCols() { return Array.from({ length: 53 }); }\n get skeletonRows() { return Array.from({ length: 7 }); }\n get dayLabels() { return DAY_LABELS; }\n\n get cssVars(): Record<string, string> {\n const t: HeatmapTheme = { ...DEFAULT_THEME, ...this.theme };\n return {\n \"--ghm-color-l0\": t.colorL0, \"--ghm-color-l1\": t.colorL1,\n \"--ghm-color-l2\": t.colorL2, \"--ghm-color-l3\": t.colorL3,\n \"--ghm-color-l4\": t.colorL4, \"--ghm-text\": t.textColor,\n \"--ghm-tooltip-bg\": t.tooltipBg, \"--ghm-tooltip-border\": t.tooltipBorderColor,\n \"--ghm-tooltip-text\": t.tooltipTextColor, \"--ghm-font\": t.fontFamily,\n \"--ghm-fs\": t.fontSize,\n };\n }\n\n get monthLabels() {\n return this.showMonthLabels && this.calendarData ? buildMonthLabels(this.calendarData.weeks) : [];\n }\n\n private cdr = inject(ChangeDetectorRef);\n private el = inject(ElementRef);\n private renderer = inject(Renderer2);\n\n ngOnInit() { this.injectStyles(); this.load(); }\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes[\"apiUrl\"] || changes[\"data\"]) this.load();\n }\n\n private async load() {\n if (this.data) {\n this.calendarData = this.data;\n this.status = \"success\";\n this.cdr.markForCheck();\n return;\n }\n const resolver: (() => Promise<HeatmapData>) | null =\n this.fetchData ?? (this.apiUrl ? () => fetch(this.apiUrl!).then((r) => r.json()) : null);\n if (!resolver) return;\n\n this.status = \"loading\";\n this.cdr.markForCheck();\n try {\n this.calendarData = await resolver();\n this.status = \"success\";\n } catch (e: unknown) {\n this.error = e instanceof Error ? e : new Error(String(e));\n this.status = \"error\";\n }\n this.cdr.markForCheck();\n }\n\n formatTooltip(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 onMouseEnter(e: MouseEvent, day: HeatmapDay) {\n const target = e.currentTarget as HTMLElement;\n const r = target.getBoundingClientRect();\n const wr = this.el.nativeElement.getBoundingClientRect();\n this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };\n this.cdr.markForCheck();\n }\n\n onCellClick(day: HeatmapDay) {\n this.tappedDay = this.tappedDay?.date === day.date ? null : day;\n this.dayClick.emit(day);\n this.cdr.markForCheck();\n }\n\n private injectStyles() {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(\"ghm-style\")) return;\n const style = this.renderer.createElement(\"style\") as HTMLStyleElement;\n style.id = \"ghm-style\";\n style.textContent = `\n [data-git-heatmap] {\n display:block; position:relative; width:100%;\n font-size:var(--ghm-fs); font-family:var(--ghm-font);\n color:var(--ghm-text); user-select:none; box-sizing:border-box;\n }\n [data-git-heatmap] * { box-sizing:border-box; }\n `;\n this.renderer.appendChild(document.head, style);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/lib/git-heatmap.component.ts"],"sourcesContent":["export { GitHeatmapComponent } from \"./lib/git-heatmap.component\";\n","import {\n Component, Input, Output, EventEmitter,\n OnInit, OnChanges,\n ChangeDetectionStrategy, ChangeDetectorRef,\n ViewEncapsulation, ElementRef, Renderer2,\n inject,\n} from \"@angular/core\";\nimport { NgFor, NgIf } from \"@angular/common\";\nimport {\n buildMonthLabels, CELL, GAP, DAY_LABELS,\n DEFAULT_LEVELS, DEFAULT_THEME,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, HeatmapTheme, LevelConfig, MonthLabel } from \"@rsalianto/git-heatmap-core\";\n\nconst DAY_LABEL_W = 32;\nconst MONTH_LABEL_H = 16;\n\n@Component({\n selector: \"git-heatmap\",\n standalone: true,\n imports: [NgFor, NgIf],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"ghm-wrapper\" [attr.data-git-heatmap]=\"''\" [style]=\"cssVars\" [attr.aria-label]=\"label\" style=\"position:relative;\">\n\n <ng-container *ngIf=\"error\">\n <p style=\"opacity:0.6; padding:1rem 0;\">Could not load contribution data.</p>\n </ng-container>\n\n <!-- Skeleton -->\n <ng-container *ngIf=\"!error && (status === 'loading' || status === 'idle')\">\n <div *ngIf=\"showTotal\" style=\"height:14px; width:160px; background:var(--ghm-text); border-radius:4px; margin-bottom:12px; opacity:0.2;\"></div>\n <svg [attr.width]=\"svgWidth(53)\" [attr.height]=\"svgHeight\" style=\"display:block; opacity:0.4;\">\n <ng-container *ngFor=\"let wi of skeletonCols\">\n <rect\n *ngFor=\"let dow of skeletonRows\"\n [attr.x]=\"offsetX + wi * step\" [attr.y]=\"offsetY + dow * step\"\n [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\"\n fill=\"var(--ghm-color-l0)\"\n />\n </ng-container>\n </svg>\n </ng-container>\n\n <!-- Loaded -->\n <ng-container *ngIf=\"!error && status === 'success' && calendarData\">\n <p *ngIf=\"showTotal\" style=\"margin-bottom:12px; color:var(--ghm-text);\">\n {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year\n </p>\n\n <div #scrollRef style=\"overflow-x:auto; overflow-y:visible; padding-bottom:4px;\">\n <svg\n [attr.width]=\"svgWidth(calendarData.weeks.length)\"\n [attr.height]=\"svgHeight\"\n style=\"display:block; overflow:visible;\"\n role=\"img\" [attr.aria-label]=\"label\"\n >\n <!-- Month labels -->\n <text\n *ngFor=\"let ml of monthLabels\"\n [attr.x]=\"offsetX + ml.col * step\"\n [attr.y]=\"monthLabelH - 4\"\n fill=\"var(--ghm-text)\" font-size=\"10\"\n >{{ ml.label }}</text>\n\n <!-- Day labels -->\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <text\n *ngIf=\"dayLabels[dow]\"\n [attr.x]=\"dayLabelW - 6\"\n [attr.y]=\"offsetY + dow * step + cellSize\"\n fill=\"var(--ghm-text)\" font-size=\"10\" text-anchor=\"end\"\n >{{ dayLabels[dow] }}</text>\n </ng-container>\n\n <!-- Grid -->\n <ng-container *ngFor=\"let week of calendarData.weeks; let wi = index\">\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <rect\n *ngIf=\"week.days[dow]?.date\"\n [attr.x]=\"offsetX + wi * step\"\n [attr.y]=\"offsetY + dow * step\"\n [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\"\n [attr.fill]=\"'var(--ghm-color-l' + week.days[dow].level + ')'\"\n style=\"cursor:pointer;\"\n [attr.aria-label]=\"formatTooltip(week.days[dow])\"\n role=\"button\"\n (mouseenter)=\"onMouseEnter($event, week.days[dow])\"\n (mouseleave)=\"tooltip = null; cdr.markForCheck()\"\n (click)=\"onCellClick(week.days[dow])\"\n />\n </ng-container>\n </ng-container>\n </svg>\n </div>\n\n <!-- Mobile tapped info -->\n <div *ngIf=\"tappedDay\" style=\"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 {{ formatTooltip(tappedDay) }}\n </div>\n\n <!-- Legend -->\n <div *ngIf=\"showLegend\" 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>\n <svg *ngFor=\"let lvl of levels; let i = index\" [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" style=\"display:block;\">\n <rect [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\" [attr.fill]=\"'var(--ghm-color-l' + i + ')'\">\n <title>{{ lvl.label }}</title>\n </rect>\n </svg>\n <span>More</span>\n </div>\n </div>\n\n <!-- Tooltip -->\n <div *ngIf=\"tooltip\" [style]=\"{\n pointerEvents:'none', position:'absolute', zIndex:50,\n left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',\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 fontSize:'0.9em', borderRadius:'4px', padding:'3px 8px', whiteSpace:'nowrap'\n }\">\n {{ formatTooltip(tooltip.day) }}\n </div>\n </ng-container>\n\n </div>\n `,\n})\nexport class GitHeatmapComponent implements OnInit, OnChanges {\n @Input() data?: HeatmapData;\n @Input() apiUrl?: string;\n @Input() fetchData?: () => Promise<HeatmapData>;\n @Input() levels: LevelConfig[] = DEFAULT_LEVELS;\n @Input() cellSize = CELL;\n @Input() cellGap = GAP;\n @Input() cellRadius = 2;\n @Input() showTotal = true;\n @Input() showLegend = true;\n @Input() showMonthLabels = true;\n @Input() showDayLabels = true;\n @Input() theme: Partial<HeatmapTheme> = {};\n @Input() label = \"Contribution heatmap\";\n @Output() dayClick = new EventEmitter<HeatmapDay>();\n\n calendarData: HeatmapData | null = null;\n status: \"idle\" | \"loading\" | \"success\" | \"error\" = \"idle\";\n error: Error | null = null;\n tappedDay: HeatmapDay | null = null;\n tooltip: { day: HeatmapDay; x: number; y: number } | null = null;\n\n readonly dayLabelW = DAY_LABEL_W;\n readonly monthLabelH = MONTH_LABEL_H;\n readonly dayLabels = DAY_LABELS;\n readonly skeletonCols = Array.from({ length: 53 }, (_, i) => i);\n readonly skeletonRows = Array.from({ length: 7 }, (_, i) => i);\n\n get step() { return this.cellSize + this.cellGap; }\n get offsetX() { return this.showDayLabels ? DAY_LABEL_W : 0; }\n get offsetY() { return this.showMonthLabels ? MONTH_LABEL_H + this.cellGap : 0; }\n get svgHeight(){ return 7 * this.step - this.cellGap + this.offsetY; }\n svgWidth(weeks: number) { return weeks * this.step + this.offsetX; }\n\n get cssVars(): string {\n const t: HeatmapTheme = { ...DEFAULT_THEME, ...this.theme };\n return [\n `--ghm-color-l0:${t.colorL0}`, `--ghm-color-l1:${t.colorL1}`,\n `--ghm-color-l2:${t.colorL2}`, `--ghm-color-l3:${t.colorL3}`,\n `--ghm-color-l4:${t.colorL4}`, `--ghm-text:${t.textColor}`,\n `--ghm-tooltip-bg:${t.tooltipBg}`, `--ghm-tooltip-border:${t.tooltipBorderColor}`,\n `--ghm-tooltip-text:${t.tooltipTextColor}`,\n `--ghm-font:${t.fontFamily}`, `--ghm-fs:${t.fontSize}`,\n ].join(\";\");\n }\n\n get monthLabels(): MonthLabel[] {\n return this.showMonthLabels && this.calendarData ? buildMonthLabels(this.calendarData.weeks) : [];\n }\n\n private cdr = inject(ChangeDetectorRef);\n private el = inject(ElementRef);\n private renderer = inject(Renderer2);\n\n ngOnInit() { this.injectStyles(); this.load(); }\n ngOnChanges() { this.load(); }\n\n private async load() {\n if (this.data) {\n this.calendarData = this.data; this.status = \"success\"; this.cdr.markForCheck(); return;\n }\n const resolver = this.fetchData ?? (this.apiUrl ? () => fetch(this.apiUrl!).then(r => r.json()) : null);\n if (!resolver) return;\n this.status = \"loading\"; this.cdr.markForCheck();\n try {\n this.calendarData = await resolver(); this.status = \"success\";\n } catch (e: unknown) {\n this.error = e instanceof Error ? e : new Error(String(e)); this.status = \"error\";\n }\n this.cdr.markForCheck();\n }\n\n formatTooltip(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 onMouseEnter(e: MouseEvent, day: HeatmapDay) {\n const r = (e.currentTarget as SVGRectElement).getBoundingClientRect();\n const wr = this.el.nativeElement.getBoundingClientRect();\n this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };\n this.cdr.markForCheck();\n }\n\n onCellClick(day: HeatmapDay) {\n this.tappedDay = this.tappedDay?.date === day.date ? null : day;\n this.dayClick.emit(day);\n this.cdr.markForCheck();\n }\n\n private injectStyles() {\n if (typeof document === \"undefined\" || document.getElementById(\"ghm-style\")) return;\n const style = this.renderer.createElement(\"style\") as HTMLStyleElement;\n style.id = \"ghm-style\";\n style.textContent = `\n [data-git-heatmap] {\n display:block; position:relative; width:100%;\n font-size:var(--ghm-fs); font-family:var(--ghm-font);\n color:var(--ghm-text); user-select:none; box-sizing:border-box;\n }\n [data-git-heatmap] * { box-sizing:border-box; }\n [data-git-heatmap] rect { transition:opacity 0.15s; cursor:pointer; }\n [data-git-heatmap] rect:hover { opacity:0.7; }\n `;\n this.renderer.appendChild(document.head, style);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAMO;AACP,oBAA4B;AAC5B,8BAGO;AAGP,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAqHf,IAAM,sBAAN,MAAuD;AAAA,EAAvD;AAII,kBAAwB;AACxB,oBAAW;AACX,mBAAU;AACV,sBAAa;AACb,qBAAY;AACZ,sBAAa;AACb,2BAAkB;AAClB,yBAAgB;AAChB,iBAA+B,CAAC;AAChC,iBAAQ;AACP,oBAAW,IAAI,yBAAyB;AAElD,wBAAmC;AACnC,kBAAmD;AACnD,iBAAsB;AACtB,qBAA+B;AAC/B,mBAA4D;AAE5D,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,eAAe,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC;AAC9D,SAAS,eAAe,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC;AAwB7D,SAAQ,UAAU,oBAAO,6BAAiB;AAC1C,SAAQ,SAAU,oBAAO,sBAAU;AACnC,SAAQ,eAAW,oBAAO,qBAAS;AAAA;AAAA,EAxBnC,IAAI,OAAU;AAAE,WAAO,KAAK,WAAW,KAAK;AAAA,EAAS;AAAA,EACrD,IAAI,UAAU;AAAE,WAAO,KAAK,gBAAgB,cAAc;AAAA,EAAG;AAAA,EAC7D,IAAI,UAAU;AAAE,WAAO,KAAK,kBAAkB,gBAAgB,KAAK,UAAU;AAAA,EAAG;AAAA,EAChF,IAAI,YAAW;AAAE,WAAO,IAAI,KAAK,OAAO,KAAK,UAAU,KAAK;AAAA,EAAS;AAAA,EACrE,SAAS,OAAe;AAAE,WAAO,QAAQ,KAAK,OAAO,KAAK;AAAA,EAAS;AAAA,EAEnE,IAAI,UAAkB;AACpB,UAAM,IAAkB,EAAE,GAAG,uCAAe,GAAG,KAAK,MAAM;AAC1D,WAAO;AAAA,MACL,kBAAkB,EAAE,OAAO;AAAA,MAAI,kBAAkB,EAAE,OAAO;AAAA,MAC1D,kBAAkB,EAAE,OAAO;AAAA,MAAI,kBAAkB,EAAE,OAAO;AAAA,MAC1D,kBAAkB,EAAE,OAAO;AAAA,MAAI,cAAc,EAAE,SAAS;AAAA,MACxD,oBAAoB,EAAE,SAAS;AAAA,MAAI,wBAAwB,EAAE,kBAAkB;AAAA,MAC/E,sBAAsB,EAAE,gBAAgB;AAAA,MACxC,cAAc,EAAE,UAAU;AAAA,MAAI,YAAY,EAAE,QAAQ;AAAA,IACtD,EAAE,KAAK,GAAG;AAAA,EACZ;AAAA,EAEA,IAAI,cAA4B;AAC9B,WAAO,KAAK,mBAAmB,KAAK,mBAAe,0CAAiB,KAAK,aAAa,KAAK,IAAI,CAAC;AAAA,EAClG;AAAA,EAMA,WAAc;AAAE,SAAK,aAAa;AAAG,SAAK,KAAK;AAAA,EAAG;AAAA,EAClD,cAAc;AAAE,SAAK,KAAK;AAAA,EAAG;AAAA,EAE7B,MAAc,OAAO;AACnB,QAAI,KAAK,MAAM;AACb,WAAK,eAAe,KAAK;AAAM,WAAK,SAAS;AAAW,WAAK,IAAI,aAAa;AAAG;AAAA,IACnF;AACA,UAAM,WAAW,KAAK,cAAc,KAAK,SAAS,MAAM,MAAM,KAAK,MAAO,EAAE,KAAK,OAAK,EAAE,KAAK,CAAC,IAAI;AAClG,QAAI,CAAC,SAAU;AACf,SAAK,SAAS;AAAW,SAAK,IAAI,aAAa;AAC/C,QAAI;AACF,WAAK,eAAe,MAAM,SAAS;AAAG,WAAK,SAAS;AAAA,IACtD,SAAS,GAAY;AACnB,WAAK,QAAQ,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AAAG,WAAK,SAAS;AAAA,IAC5E;AACA,SAAK,IAAI,aAAa;AAAA,EACxB;AAAA,EAEA,cAAc,KAAyB;AACrC,WAAO,IAAI,UAAU,IACjB,uBAAuB,IAAI,IAAI,KAC/B,GAAG,IAAI,KAAK,gBAAgB,IAAI,QAAQ,IAAI,MAAM,EAAE,OAAO,IAAI,IAAI;AAAA,EACzE;AAAA,EAEA,aAAa,GAAe,KAAiB;AAC3C,UAAM,IAAM,EAAE,cAAiC,sBAAsB;AACrE,UAAM,KAAK,KAAK,GAAG,cAAc,sBAAsB;AACvD,SAAK,UAAU,EAAE,KAAK,GAAG,EAAE,OAAO,GAAG,OAAO,KAAK,WAAW,GAAG,GAAG,EAAE,MAAM,GAAG,IAAI;AACjF,SAAK,IAAI,aAAa;AAAA,EACxB;AAAA,EAEA,YAAY,KAAiB;AAC3B,SAAK,YAAY,KAAK,WAAW,SAAS,IAAI,OAAO,OAAO;AAC5D,SAAK,SAAS,KAAK,GAAG;AACtB,SAAK,IAAI,aAAa;AAAA,EACxB;AAAA,EAEQ,eAAe;AACrB,QAAI,OAAO,aAAa,eAAe,SAAS,eAAe,WAAW,EAAG;AAC7E,UAAM,QAAQ,KAAK,SAAS,cAAc,OAAO;AACjD,UAAM,KAAK;AACX,UAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUpB,SAAK,SAAS,YAAY,SAAS,MAAM,KAAK;AAAA,EAChD;AACF;AA1GW;AAAA,MAAR,mBAAM;AAAA,GADI,oBACF;AACA;AAAA,MAAR,mBAAM;AAAA,GAFI,oBAEF;AACA;AAAA,MAAR,mBAAM;AAAA,GAHI,oBAGF;AACA;AAAA,MAAR,mBAAM;AAAA,GAJI,oBAIF;AACA;AAAA,MAAR,mBAAM;AAAA,GALI,oBAKF;AACA;AAAA,MAAR,mBAAM;AAAA,GANI,oBAMF;AACA;AAAA,MAAR,mBAAM;AAAA,GAPI,oBAOF;AACA;AAAA,MAAR,mBAAM;AAAA,GARI,oBAQF;AACA;AAAA,MAAR,mBAAM;AAAA,GATI,oBASF;AACA;AAAA,MAAR,mBAAM;AAAA,GAVI,oBAUF;AACA;AAAA,MAAR,mBAAM;AAAA,GAXI,oBAWF;AACA;AAAA,MAAR,mBAAM;AAAA,GAZI,oBAYF;AACA;AAAA,MAAR,mBAAM;AAAA,GAbI,oBAaF;AACC;AAAA,MAAT,oBAAO;AAAA,GAdG,oBAcD;AAdC,sBAAN;AAAA,MAnHN,uBAAU;AAAA,IACT,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,SAAS,CAAC,qBAAO,kBAAI;AAAA,IACrB,eAAe,8BAAkB;AAAA,IACjC,iBAAiB,oCAAwB;AAAA,IACzC,UAAU;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;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4GZ,CAAC;AAAA,GACY;","names":[]}
package/dist/index.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  Renderer2,
23
23
  inject
24
24
  } from "@angular/core";
25
- import { NgFor, NgIf, NgStyle } from "@angular/common";
25
+ import { NgFor, NgIf } from "@angular/common";
26
26
  import {
27
27
  buildMonthLabels,
28
28
  CELL,
@@ -31,6 +31,8 @@ import {
31
31
  DEFAULT_LEVELS,
32
32
  DEFAULT_THEME
33
33
  } from "@rsalianto/git-heatmap-core";
34
+ var DAY_LABEL_W = 32;
35
+ var MONTH_LABEL_H = 16;
34
36
  var GitHeatmapComponent = class {
35
37
  constructor() {
36
38
  this.levels = DEFAULT_LEVELS;
@@ -49,6 +51,11 @@ var GitHeatmapComponent = class {
49
51
  this.error = null;
50
52
  this.tappedDay = null;
51
53
  this.tooltip = null;
54
+ this.dayLabelW = DAY_LABEL_W;
55
+ this.monthLabelH = MONTH_LABEL_H;
56
+ this.dayLabels = DAY_LABELS;
57
+ this.skeletonCols = Array.from({ length: 53 }, (_, i) => i);
58
+ this.skeletonRows = Array.from({ length: 7 }, (_, i) => i);
52
59
  this.cdr = inject(ChangeDetectorRef);
53
60
  this.el = inject(ElementRef);
54
61
  this.renderer = inject(Renderer2);
@@ -56,30 +63,33 @@ var GitHeatmapComponent = class {
56
63
  get step() {
57
64
  return this.cellSize + this.cellGap;
58
65
  }
59
- get skeletonCols() {
60
- return Array.from({ length: 53 });
66
+ get offsetX() {
67
+ return this.showDayLabels ? DAY_LABEL_W : 0;
61
68
  }
62
- get skeletonRows() {
63
- return Array.from({ length: 7 });
69
+ get offsetY() {
70
+ return this.showMonthLabels ? MONTH_LABEL_H + this.cellGap : 0;
64
71
  }
65
- get dayLabels() {
66
- return DAY_LABELS;
72
+ get svgHeight() {
73
+ return 7 * this.step - this.cellGap + this.offsetY;
74
+ }
75
+ svgWidth(weeks) {
76
+ return weeks * this.step + this.offsetX;
67
77
  }
68
78
  get cssVars() {
69
79
  const t = { ...DEFAULT_THEME, ...this.theme };
70
- return {
71
- "--ghm-color-l0": t.colorL0,
72
- "--ghm-color-l1": t.colorL1,
73
- "--ghm-color-l2": t.colorL2,
74
- "--ghm-color-l3": t.colorL3,
75
- "--ghm-color-l4": t.colorL4,
76
- "--ghm-text": t.textColor,
77
- "--ghm-tooltip-bg": t.tooltipBg,
78
- "--ghm-tooltip-border": t.tooltipBorderColor,
79
- "--ghm-tooltip-text": t.tooltipTextColor,
80
- "--ghm-font": t.fontFamily,
81
- "--ghm-fs": t.fontSize
82
- };
80
+ return [
81
+ `--ghm-color-l0:${t.colorL0}`,
82
+ `--ghm-color-l1:${t.colorL1}`,
83
+ `--ghm-color-l2:${t.colorL2}`,
84
+ `--ghm-color-l3:${t.colorL3}`,
85
+ `--ghm-color-l4:${t.colorL4}`,
86
+ `--ghm-text:${t.textColor}`,
87
+ `--ghm-tooltip-bg:${t.tooltipBg}`,
88
+ `--ghm-tooltip-border:${t.tooltipBorderColor}`,
89
+ `--ghm-tooltip-text:${t.tooltipTextColor}`,
90
+ `--ghm-font:${t.fontFamily}`,
91
+ `--ghm-fs:${t.fontSize}`
92
+ ].join(";");
83
93
  }
84
94
  get monthLabels() {
85
95
  return this.showMonthLabels && this.calendarData ? buildMonthLabels(this.calendarData.weeks) : [];
@@ -88,8 +98,8 @@ var GitHeatmapComponent = class {
88
98
  this.injectStyles();
89
99
  this.load();
90
100
  }
91
- ngOnChanges(changes) {
92
- if (changes["apiUrl"] || changes["data"]) this.load();
101
+ ngOnChanges() {
102
+ this.load();
93
103
  }
94
104
  async load() {
95
105
  if (this.data) {
@@ -115,8 +125,7 @@ var GitHeatmapComponent = class {
115
125
  return day.count === 0 ? `No contributions on ${day.date}` : `${day.count} contribution${day.count > 1 ? "s" : ""} on ${day.date}`;
116
126
  }
117
127
  onMouseEnter(e, day) {
118
- const target = e.currentTarget;
119
- const r = target.getBoundingClientRect();
128
+ const r = e.currentTarget.getBoundingClientRect();
120
129
  const wr = this.el.nativeElement.getBoundingClientRect();
121
130
  this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };
122
131
  this.cdr.markForCheck();
@@ -127,8 +136,7 @@ var GitHeatmapComponent = class {
127
136
  this.cdr.markForCheck();
128
137
  }
129
138
  injectStyles() {
130
- if (typeof document === "undefined") return;
131
- if (document.getElementById("ghm-style")) return;
139
+ if (typeof document === "undefined" || document.getElementById("ghm-style")) return;
132
140
  const style = this.renderer.createElement("style");
133
141
  style.id = "ghm-style";
134
142
  style.textContent = `
@@ -138,6 +146,8 @@ var GitHeatmapComponent = class {
138
146
  color:var(--ghm-text); user-select:none; box-sizing:border-box;
139
147
  }
140
148
  [data-git-heatmap] * { box-sizing:border-box; }
149
+ [data-git-heatmap] rect { transition:opacity 0.15s; cursor:pointer; }
150
+ [data-git-heatmap] rect:hover { opacity:0.7; }
141
151
  `;
142
152
  this.renderer.appendChild(document.head, style);
143
153
  }
@@ -188,93 +198,103 @@ GitHeatmapComponent = __decorateClass([
188
198
  Component({
189
199
  selector: "git-heatmap",
190
200
  standalone: true,
191
- imports: [NgFor, NgIf, NgStyle],
201
+ imports: [NgFor, NgIf],
192
202
  encapsulation: ViewEncapsulation.None,
193
203
  changeDetection: ChangeDetectionStrategy.OnPush,
194
204
  template: `
195
- <div class="ghm-wrapper" [attr.data-git-heatmap]="''" [ngStyle]="cssVars" [attr.aria-label]="label">
205
+ <div class="ghm-wrapper" [attr.data-git-heatmap]="''" [style]="cssVars" [attr.aria-label]="label" style="position:relative;">
196
206
 
197
207
  <ng-container *ngIf="error">
198
208
  <p style="opacity:0.6; padding:1rem 0;">Could not load contribution data.</p>
199
209
  </ng-container>
200
210
 
211
+ <!-- Skeleton -->
201
212
  <ng-container *ngIf="!error && (status === 'loading' || status === 'idle')">
202
- <div *ngIf="showTotal" style="height:16px;width:160px;background:var(--ghm-text);border-radius:4px;margin-bottom:12px;opacity:0.2;"></div>
203
- <div [ngStyle]="{ display:'flex', gap: cellGap + 'px', opacity: 0.4 }">
204
- <div *ngFor="let _ of skeletonCols" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }">
205
- <div *ngFor="let __ of skeletonRows" [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l0)' }"></div>
206
- </div>
207
- </div>
213
+ <div *ngIf="showTotal" style="height:14px; width:160px; background:var(--ghm-text); border-radius:4px; margin-bottom:12px; opacity:0.2;"></div>
214
+ <svg [attr.width]="svgWidth(53)" [attr.height]="svgHeight" style="display:block; opacity:0.4;">
215
+ <ng-container *ngFor="let wi of skeletonCols">
216
+ <rect
217
+ *ngFor="let dow of skeletonRows"
218
+ [attr.x]="offsetX + wi * step" [attr.y]="offsetY + dow * step"
219
+ [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius"
220
+ fill="var(--ghm-color-l0)"
221
+ />
222
+ </ng-container>
223
+ </svg>
208
224
  </ng-container>
209
225
 
226
+ <!-- Loaded -->
210
227
  <ng-container *ngIf="!error && status === 'success' && calendarData">
211
- <p *ngIf="showTotal" style="margin-bottom:12px;color:var(--ghm-text);">
228
+ <p *ngIf="showTotal" style="margin-bottom:12px; color:var(--ghm-text);">
212
229
  {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year
213
230
  </p>
214
231
 
215
- <div #scrollRef style="overflow-x:auto;overflow-y:visible;padding-bottom:4px;">
216
- <div [ngStyle]="{ display:'inline-flex', flexDirection:'column', minWidth: (calendarData.weeks.length * step) + 'px' }">
217
-
218
- <div *ngIf="showMonthLabels" [ngStyle]="{ display:'flex', marginBottom: cellGap + 'px', marginLeft: showDayLabels ? '32px' : '0' }">
219
- <div *ngFor="let ml of monthLabels; let idx = index"
220
- [ngStyle]="{ width: ((monthLabels[idx+1]?.col ?? calendarData.weeks.length) - ml.col) * step + 'px', whiteSpace:'nowrap', fontSize:'0.9em' }">
221
- {{ ml.label }}
222
- </div>
223
- </div>
232
+ <div #scrollRef style="overflow-x:auto; overflow-y:visible; padding-bottom:4px;">
233
+ <svg
234
+ [attr.width]="svgWidth(calendarData.weeks.length)"
235
+ [attr.height]="svgHeight"
236
+ style="display:block; overflow:visible;"
237
+ role="img" [attr.aria-label]="label"
238
+ >
239
+ <!-- Month labels -->
240
+ <text
241
+ *ngFor="let ml of monthLabels"
242
+ [attr.x]="offsetX + ml.col * step"
243
+ [attr.y]="monthLabelH - 4"
244
+ fill="var(--ghm-text)" font-size="10"
245
+ >{{ ml.label }}</text>
224
246
 
225
- <div style="display:flex;">
226
- <div *ngIf="showDayLabels" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px', marginRight:'6px' }">
227
- <div *ngFor="let dow of [0,1,2,3,4,5,6]"
228
- [ngStyle]="{ height: cellSize + 'px', lineHeight: cellSize + 'px', width:'26px', textAlign:'right', paddingRight:'4px', fontSize:'0.9em' }">
229
- {{ dayLabels[dow] || '' }}
230
- </div>
231
- </div>
247
+ <!-- Day labels -->
248
+ <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
249
+ <text
250
+ *ngIf="dayLabels[dow]"
251
+ [attr.x]="dayLabelW - 6"
252
+ [attr.y]="offsetY + dow * step + cellSize"
253
+ fill="var(--ghm-text)" font-size="10" text-anchor="end"
254
+ >{{ dayLabels[dow] }}</text>
255
+ </ng-container>
232
256
 
233
- <div [ngStyle]="{ display:'flex', gap: cellGap + 'px' }">
234
- <div *ngFor="let week of calendarData.weeks; let wi = index" [ngStyle]="{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }">
235
- <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
236
- <div *ngIf="week.days[dow]?.date; else emptyCell"
237
- [ngStyle]="{
238
- width: cellSize + 'px', height: cellSize + 'px',
239
- borderRadius: cellRadius + 'px',
240
- background: 'var(--ghm-color-l' + week.days[dow].level + ')',
241
- cursor: 'pointer', transition: 'opacity 0.15s'
242
- }"
243
- [attr.aria-label]="formatTooltip(week.days[dow])"
244
- role="button"
245
- tabindex="0"
246
- (mouseenter)="onMouseEnter($event, week.days[dow])"
247
- (mouseleave)="tooltip = null; cdr.markForCheck()"
248
- (mouseover)="$any($event.currentTarget).style.opacity='0.7'"
249
- (mouseout)="$any($event.currentTarget).style.opacity='1'"
250
- (click)="onCellClick(week.days[dow])">
251
- </div>
252
- <ng-template #emptyCell>
253
- <div [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px' }"></div>
254
- </ng-template>
255
- </ng-container>
256
- </div>
257
- </div>
258
- </div>
259
- </div>
257
+ <!-- Grid -->
258
+ <ng-container *ngFor="let week of calendarData.weeks; let wi = index">
259
+ <ng-container *ngFor="let dow of [0,1,2,3,4,5,6]">
260
+ <rect
261
+ *ngIf="week.days[dow]?.date"
262
+ [attr.x]="offsetX + wi * step"
263
+ [attr.y]="offsetY + dow * step"
264
+ [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius"
265
+ [attr.fill]="'var(--ghm-color-l' + week.days[dow].level + ')'"
266
+ style="cursor:pointer;"
267
+ [attr.aria-label]="formatTooltip(week.days[dow])"
268
+ role="button"
269
+ (mouseenter)="onMouseEnter($event, week.days[dow])"
270
+ (mouseleave)="tooltip = null; cdr.markForCheck()"
271
+ (click)="onCellClick(week.days[dow])"
272
+ />
273
+ </ng-container>
274
+ </ng-container>
275
+ </svg>
260
276
  </div>
261
277
 
262
- <div *ngIf="tappedDay" style="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;">
278
+ <!-- Mobile tapped info -->
279
+ <div *ngIf="tappedDay" style="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;">
263
280
  {{ formatTooltip(tappedDay) }}
264
281
  </div>
265
282
 
266
- <div *ngIf="showLegend" style="display:flex;align-items:center;justify-content:flex-end;margin-top:8px;">
267
- <div style="display:flex;align-items:center;gap:6px;font-size:0.9em;">
283
+ <!-- Legend -->
284
+ <div *ngIf="showLegend" style="display:flex; align-items:center; justify-content:flex-end; margin-top:8px;">
285
+ <div style="display:flex; align-items:center; gap:6px; font-size:0.9em;">
268
286
  <span>Less</span>
269
- <div *ngFor="let lvl of levels; let i = index"
270
- [ngStyle]="{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l' + i + ')' }"
271
- [attr.title]="lvl.label">
272
- </div>
287
+ <svg *ngFor="let lvl of levels; let i = index" [attr.width]="cellSize" [attr.height]="cellSize" style="display:block;">
288
+ <rect [attr.width]="cellSize" [attr.height]="cellSize" [attr.rx]="cellRadius" [attr.fill]="'var(--ghm-color-l' + i + ')'">
289
+ <title>{{ lvl.label }}</title>
290
+ </rect>
291
+ </svg>
273
292
  <span>More</span>
274
293
  </div>
275
294
  </div>
276
295
 
277
- <div *ngIf="tooltip" [ngStyle]="{
296
+ <!-- Tooltip -->
297
+ <div *ngIf="tooltip" [style]="{
278
298
  pointerEvents:'none', position:'absolute', zIndex:50,
279
299
  left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',
280
300
  transform:'translate(-50%,-100%)',
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/git-heatmap.component.ts"],"sourcesContent":["import {\n Component, Input, Output, EventEmitter,\n OnInit, OnChanges, SimpleChanges,\n ChangeDetectionStrategy, ChangeDetectorRef,\n ViewEncapsulation, ElementRef, Renderer2,\n inject,\n} from \"@angular/core\";\nimport { NgFor, NgIf, NgStyle } from \"@angular/common\";\nimport {\n buildMonthLabels, CELL, GAP, STEP, DAY_LABELS,\n DEFAULT_LEVELS, DEFAULT_THEME,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, HeatmapTheme, LevelConfig } from \"@rsalianto/git-heatmap-core\";\n\n@Component({\n selector: \"git-heatmap\",\n standalone: true,\n imports: [NgFor, NgIf, NgStyle],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"ghm-wrapper\" [attr.data-git-heatmap]=\"''\" [ngStyle]=\"cssVars\" [attr.aria-label]=\"label\">\n\n <ng-container *ngIf=\"error\">\n <p style=\"opacity:0.6; padding:1rem 0;\">Could not load contribution data.</p>\n </ng-container>\n\n <ng-container *ngIf=\"!error && (status === 'loading' || status === 'idle')\">\n <div *ngIf=\"showTotal\" style=\"height:16px;width:160px;background:var(--ghm-text);border-radius:4px;margin-bottom:12px;opacity:0.2;\"></div>\n <div [ngStyle]=\"{ display:'flex', gap: cellGap + 'px', opacity: 0.4 }\">\n <div *ngFor=\"let _ of skeletonCols\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }\">\n <div *ngFor=\"let __ of skeletonRows\" [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l0)' }\"></div>\n </div>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!error && status === 'success' && calendarData\">\n <p *ngIf=\"showTotal\" style=\"margin-bottom:12px;color:var(--ghm-text);\">\n {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year\n </p>\n\n <div #scrollRef style=\"overflow-x:auto;overflow-y:visible;padding-bottom:4px;\">\n <div [ngStyle]=\"{ display:'inline-flex', flexDirection:'column', minWidth: (calendarData.weeks.length * step) + 'px' }\">\n\n <div *ngIf=\"showMonthLabels\" [ngStyle]=\"{ display:'flex', marginBottom: cellGap + 'px', marginLeft: showDayLabels ? '32px' : '0' }\">\n <div *ngFor=\"let ml of monthLabels; let idx = index\"\n [ngStyle]=\"{ width: ((monthLabels[idx+1]?.col ?? calendarData.weeks.length) - ml.col) * step + 'px', whiteSpace:'nowrap', fontSize:'0.9em' }\">\n {{ ml.label }}\n </div>\n </div>\n\n <div style=\"display:flex;\">\n <div *ngIf=\"showDayLabels\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px', marginRight:'6px' }\">\n <div *ngFor=\"let dow of [0,1,2,3,4,5,6]\"\n [ngStyle]=\"{ height: cellSize + 'px', lineHeight: cellSize + 'px', width:'26px', textAlign:'right', paddingRight:'4px', fontSize:'0.9em' }\">\n {{ dayLabels[dow] || '' }}\n </div>\n </div>\n\n <div [ngStyle]=\"{ display:'flex', gap: cellGap + 'px' }\">\n <div *ngFor=\"let week of calendarData.weeks; let wi = index\" [ngStyle]=\"{ display:'flex', flexDirection:'column', gap: cellGap + 'px' }\">\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <div *ngIf=\"week.days[dow]?.date; else emptyCell\"\n [ngStyle]=\"{\n width: cellSize + 'px', height: cellSize + 'px',\n borderRadius: cellRadius + 'px',\n background: 'var(--ghm-color-l' + week.days[dow].level + ')',\n cursor: 'pointer', transition: 'opacity 0.15s'\n }\"\n [attr.aria-label]=\"formatTooltip(week.days[dow])\"\n role=\"button\"\n tabindex=\"0\"\n (mouseenter)=\"onMouseEnter($event, week.days[dow])\"\n (mouseleave)=\"tooltip = null; cdr.markForCheck()\"\n (mouseover)=\"$any($event.currentTarget).style.opacity='0.7'\"\n (mouseout)=\"$any($event.currentTarget).style.opacity='1'\"\n (click)=\"onCellClick(week.days[dow])\">\n </div>\n <ng-template #emptyCell>\n <div [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px' }\"></div>\n </ng-template>\n </ng-container>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div *ngIf=\"tappedDay\" style=\"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 {{ formatTooltip(tappedDay) }}\n </div>\n\n <div *ngIf=\"showLegend\" 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>\n <div *ngFor=\"let lvl of levels; let i = index\"\n [ngStyle]=\"{ width: cellSize + 'px', height: cellSize + 'px', borderRadius: cellRadius + 'px', background: 'var(--ghm-color-l' + i + ')' }\"\n [attr.title]=\"lvl.label\">\n </div>\n <span>More</span>\n </div>\n </div>\n\n <div *ngIf=\"tooltip\" [ngStyle]=\"{\n pointerEvents:'none', position:'absolute', zIndex:50,\n left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',\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 fontSize:'0.9em', borderRadius:'4px', padding:'3px 8px', whiteSpace:'nowrap'\n }\">\n {{ formatTooltip(tooltip.day) }}\n </div>\n </ng-container>\n\n </div>\n `,\n})\nexport class GitHeatmapComponent implements OnInit, OnChanges {\n @Input() data?: HeatmapData;\n @Input() apiUrl?: string;\n @Input() fetchData?: () => Promise<HeatmapData>;\n @Input() levels: LevelConfig[] = DEFAULT_LEVELS;\n @Input() cellSize = CELL;\n @Input() cellGap = GAP;\n @Input() cellRadius = 2;\n @Input() showTotal = true;\n @Input() showLegend = true;\n @Input() showMonthLabels = true;\n @Input() showDayLabels = true;\n @Input() theme: Partial<HeatmapTheme> = {};\n @Input() label = \"Contribution heatmap\";\n @Output() dayClick = new EventEmitter<HeatmapDay>();\n\n calendarData: HeatmapData | null = null;\n status: \"idle\" | \"loading\" | \"success\" | \"error\" = \"idle\";\n error: Error | null = null;\n tappedDay: HeatmapDay | null = null;\n tooltip: { day: HeatmapDay; x: number; y: number } | null = null;\n\n get step() { return this.cellSize + this.cellGap; }\n get skeletonCols() { return Array.from({ length: 53 }); }\n get skeletonRows() { return Array.from({ length: 7 }); }\n get dayLabels() { return DAY_LABELS; }\n\n get cssVars(): Record<string, string> {\n const t: HeatmapTheme = { ...DEFAULT_THEME, ...this.theme };\n return {\n \"--ghm-color-l0\": t.colorL0, \"--ghm-color-l1\": t.colorL1,\n \"--ghm-color-l2\": t.colorL2, \"--ghm-color-l3\": t.colorL3,\n \"--ghm-color-l4\": t.colorL4, \"--ghm-text\": t.textColor,\n \"--ghm-tooltip-bg\": t.tooltipBg, \"--ghm-tooltip-border\": t.tooltipBorderColor,\n \"--ghm-tooltip-text\": t.tooltipTextColor, \"--ghm-font\": t.fontFamily,\n \"--ghm-fs\": t.fontSize,\n };\n }\n\n get monthLabels() {\n return this.showMonthLabels && this.calendarData ? buildMonthLabels(this.calendarData.weeks) : [];\n }\n\n private cdr = inject(ChangeDetectorRef);\n private el = inject(ElementRef);\n private renderer = inject(Renderer2);\n\n ngOnInit() { this.injectStyles(); this.load(); }\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes[\"apiUrl\"] || changes[\"data\"]) this.load();\n }\n\n private async load() {\n if (this.data) {\n this.calendarData = this.data;\n this.status = \"success\";\n this.cdr.markForCheck();\n return;\n }\n const resolver: (() => Promise<HeatmapData>) | null =\n this.fetchData ?? (this.apiUrl ? () => fetch(this.apiUrl!).then((r) => r.json()) : null);\n if (!resolver) return;\n\n this.status = \"loading\";\n this.cdr.markForCheck();\n try {\n this.calendarData = await resolver();\n this.status = \"success\";\n } catch (e: unknown) {\n this.error = e instanceof Error ? e : new Error(String(e));\n this.status = \"error\";\n }\n this.cdr.markForCheck();\n }\n\n formatTooltip(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 onMouseEnter(e: MouseEvent, day: HeatmapDay) {\n const target = e.currentTarget as HTMLElement;\n const r = target.getBoundingClientRect();\n const wr = this.el.nativeElement.getBoundingClientRect();\n this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };\n this.cdr.markForCheck();\n }\n\n onCellClick(day: HeatmapDay) {\n this.tappedDay = this.tappedDay?.date === day.date ? null : day;\n this.dayClick.emit(day);\n this.cdr.markForCheck();\n }\n\n private injectStyles() {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(\"ghm-style\")) return;\n const style = this.renderer.createElement(\"style\") as HTMLStyleElement;\n style.id = \"ghm-style\";\n style.textContent = `\n [data-git-heatmap] {\n display:block; position:relative; width:100%;\n font-size:var(--ghm-fs); font-family:var(--ghm-font);\n color:var(--ghm-text); user-select:none; box-sizing:border-box;\n }\n [data-git-heatmap] * { box-sizing:border-box; }\n `;\n this.renderer.appendChild(document.head, style);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/lib/git-heatmap.component.ts"],"sourcesContent":["import {\n Component, Input, Output, EventEmitter,\n OnInit, OnChanges,\n ChangeDetectionStrategy, ChangeDetectorRef,\n ViewEncapsulation, ElementRef, Renderer2,\n inject,\n} from \"@angular/core\";\nimport { NgFor, NgIf } from \"@angular/common\";\nimport {\n buildMonthLabels, CELL, GAP, DAY_LABELS,\n DEFAULT_LEVELS, DEFAULT_THEME,\n} from \"@rsalianto/git-heatmap-core\";\nimport type { HeatmapData, HeatmapDay, HeatmapTheme, LevelConfig, MonthLabel } from \"@rsalianto/git-heatmap-core\";\n\nconst DAY_LABEL_W = 32;\nconst MONTH_LABEL_H = 16;\n\n@Component({\n selector: \"git-heatmap\",\n standalone: true,\n imports: [NgFor, NgIf],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div class=\"ghm-wrapper\" [attr.data-git-heatmap]=\"''\" [style]=\"cssVars\" [attr.aria-label]=\"label\" style=\"position:relative;\">\n\n <ng-container *ngIf=\"error\">\n <p style=\"opacity:0.6; padding:1rem 0;\">Could not load contribution data.</p>\n </ng-container>\n\n <!-- Skeleton -->\n <ng-container *ngIf=\"!error && (status === 'loading' || status === 'idle')\">\n <div *ngIf=\"showTotal\" style=\"height:14px; width:160px; background:var(--ghm-text); border-radius:4px; margin-bottom:12px; opacity:0.2;\"></div>\n <svg [attr.width]=\"svgWidth(53)\" [attr.height]=\"svgHeight\" style=\"display:block; opacity:0.4;\">\n <ng-container *ngFor=\"let wi of skeletonCols\">\n <rect\n *ngFor=\"let dow of skeletonRows\"\n [attr.x]=\"offsetX + wi * step\" [attr.y]=\"offsetY + dow * step\"\n [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\"\n fill=\"var(--ghm-color-l0)\"\n />\n </ng-container>\n </svg>\n </ng-container>\n\n <!-- Loaded -->\n <ng-container *ngIf=\"!error && status === 'success' && calendarData\">\n <p *ngIf=\"showTotal\" style=\"margin-bottom:12px; color:var(--ghm-text);\">\n {{ calendarData.totalContributions.toLocaleString() }} contributions in the last year\n </p>\n\n <div #scrollRef style=\"overflow-x:auto; overflow-y:visible; padding-bottom:4px;\">\n <svg\n [attr.width]=\"svgWidth(calendarData.weeks.length)\"\n [attr.height]=\"svgHeight\"\n style=\"display:block; overflow:visible;\"\n role=\"img\" [attr.aria-label]=\"label\"\n >\n <!-- Month labels -->\n <text\n *ngFor=\"let ml of monthLabels\"\n [attr.x]=\"offsetX + ml.col * step\"\n [attr.y]=\"monthLabelH - 4\"\n fill=\"var(--ghm-text)\" font-size=\"10\"\n >{{ ml.label }}</text>\n\n <!-- Day labels -->\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <text\n *ngIf=\"dayLabels[dow]\"\n [attr.x]=\"dayLabelW - 6\"\n [attr.y]=\"offsetY + dow * step + cellSize\"\n fill=\"var(--ghm-text)\" font-size=\"10\" text-anchor=\"end\"\n >{{ dayLabels[dow] }}</text>\n </ng-container>\n\n <!-- Grid -->\n <ng-container *ngFor=\"let week of calendarData.weeks; let wi = index\">\n <ng-container *ngFor=\"let dow of [0,1,2,3,4,5,6]\">\n <rect\n *ngIf=\"week.days[dow]?.date\"\n [attr.x]=\"offsetX + wi * step\"\n [attr.y]=\"offsetY + dow * step\"\n [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\"\n [attr.fill]=\"'var(--ghm-color-l' + week.days[dow].level + ')'\"\n style=\"cursor:pointer;\"\n [attr.aria-label]=\"formatTooltip(week.days[dow])\"\n role=\"button\"\n (mouseenter)=\"onMouseEnter($event, week.days[dow])\"\n (mouseleave)=\"tooltip = null; cdr.markForCheck()\"\n (click)=\"onCellClick(week.days[dow])\"\n />\n </ng-container>\n </ng-container>\n </svg>\n </div>\n\n <!-- Mobile tapped info -->\n <div *ngIf=\"tappedDay\" style=\"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 {{ formatTooltip(tappedDay) }}\n </div>\n\n <!-- Legend -->\n <div *ngIf=\"showLegend\" 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>\n <svg *ngFor=\"let lvl of levels; let i = index\" [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" style=\"display:block;\">\n <rect [attr.width]=\"cellSize\" [attr.height]=\"cellSize\" [attr.rx]=\"cellRadius\" [attr.fill]=\"'var(--ghm-color-l' + i + ')'\">\n <title>{{ lvl.label }}</title>\n </rect>\n </svg>\n <span>More</span>\n </div>\n </div>\n\n <!-- Tooltip -->\n <div *ngIf=\"tooltip\" [style]=\"{\n pointerEvents:'none', position:'absolute', zIndex:50,\n left: tooltip.x + 'px', top: (tooltip.y - 6) + 'px',\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 fontSize:'0.9em', borderRadius:'4px', padding:'3px 8px', whiteSpace:'nowrap'\n }\">\n {{ formatTooltip(tooltip.day) }}\n </div>\n </ng-container>\n\n </div>\n `,\n})\nexport class GitHeatmapComponent implements OnInit, OnChanges {\n @Input() data?: HeatmapData;\n @Input() apiUrl?: string;\n @Input() fetchData?: () => Promise<HeatmapData>;\n @Input() levels: LevelConfig[] = DEFAULT_LEVELS;\n @Input() cellSize = CELL;\n @Input() cellGap = GAP;\n @Input() cellRadius = 2;\n @Input() showTotal = true;\n @Input() showLegend = true;\n @Input() showMonthLabels = true;\n @Input() showDayLabels = true;\n @Input() theme: Partial<HeatmapTheme> = {};\n @Input() label = \"Contribution heatmap\";\n @Output() dayClick = new EventEmitter<HeatmapDay>();\n\n calendarData: HeatmapData | null = null;\n status: \"idle\" | \"loading\" | \"success\" | \"error\" = \"idle\";\n error: Error | null = null;\n tappedDay: HeatmapDay | null = null;\n tooltip: { day: HeatmapDay; x: number; y: number } | null = null;\n\n readonly dayLabelW = DAY_LABEL_W;\n readonly monthLabelH = MONTH_LABEL_H;\n readonly dayLabels = DAY_LABELS;\n readonly skeletonCols = Array.from({ length: 53 }, (_, i) => i);\n readonly skeletonRows = Array.from({ length: 7 }, (_, i) => i);\n\n get step() { return this.cellSize + this.cellGap; }\n get offsetX() { return this.showDayLabels ? DAY_LABEL_W : 0; }\n get offsetY() { return this.showMonthLabels ? MONTH_LABEL_H + this.cellGap : 0; }\n get svgHeight(){ return 7 * this.step - this.cellGap + this.offsetY; }\n svgWidth(weeks: number) { return weeks * this.step + this.offsetX; }\n\n get cssVars(): string {\n const t: HeatmapTheme = { ...DEFAULT_THEME, ...this.theme };\n return [\n `--ghm-color-l0:${t.colorL0}`, `--ghm-color-l1:${t.colorL1}`,\n `--ghm-color-l2:${t.colorL2}`, `--ghm-color-l3:${t.colorL3}`,\n `--ghm-color-l4:${t.colorL4}`, `--ghm-text:${t.textColor}`,\n `--ghm-tooltip-bg:${t.tooltipBg}`, `--ghm-tooltip-border:${t.tooltipBorderColor}`,\n `--ghm-tooltip-text:${t.tooltipTextColor}`,\n `--ghm-font:${t.fontFamily}`, `--ghm-fs:${t.fontSize}`,\n ].join(\";\");\n }\n\n get monthLabels(): MonthLabel[] {\n return this.showMonthLabels && this.calendarData ? buildMonthLabels(this.calendarData.weeks) : [];\n }\n\n private cdr = inject(ChangeDetectorRef);\n private el = inject(ElementRef);\n private renderer = inject(Renderer2);\n\n ngOnInit() { this.injectStyles(); this.load(); }\n ngOnChanges() { this.load(); }\n\n private async load() {\n if (this.data) {\n this.calendarData = this.data; this.status = \"success\"; this.cdr.markForCheck(); return;\n }\n const resolver = this.fetchData ?? (this.apiUrl ? () => fetch(this.apiUrl!).then(r => r.json()) : null);\n if (!resolver) return;\n this.status = \"loading\"; this.cdr.markForCheck();\n try {\n this.calendarData = await resolver(); this.status = \"success\";\n } catch (e: unknown) {\n this.error = e instanceof Error ? e : new Error(String(e)); this.status = \"error\";\n }\n this.cdr.markForCheck();\n }\n\n formatTooltip(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 onMouseEnter(e: MouseEvent, day: HeatmapDay) {\n const r = (e.currentTarget as SVGRectElement).getBoundingClientRect();\n const wr = this.el.nativeElement.getBoundingClientRect();\n this.tooltip = { day, x: r.left - wr.left + this.cellSize / 2, y: r.top - wr.top };\n this.cdr.markForCheck();\n }\n\n onCellClick(day: HeatmapDay) {\n this.tappedDay = this.tappedDay?.date === day.date ? null : day;\n this.dayClick.emit(day);\n this.cdr.markForCheck();\n }\n\n private injectStyles() {\n if (typeof document === \"undefined\" || document.getElementById(\"ghm-style\")) return;\n const style = this.renderer.createElement(\"style\") as HTMLStyleElement;\n style.id = \"ghm-style\";\n style.textContent = `\n [data-git-heatmap] {\n display:block; position:relative; width:100%;\n font-size:var(--ghm-fs); font-family:var(--ghm-font);\n color:var(--ghm-text); user-select:none; box-sizing:border-box;\n }\n [data-git-heatmap] * { box-sizing:border-box; }\n [data-git-heatmap] rect { transition:opacity 0.15s; cursor:pointer; }\n [data-git-heatmap] rect:hover { opacity:0.7; }\n `;\n this.renderer.appendChild(document.head, style);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsalianto/git-heatmap-angular",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Angular component for git contribution heatmap",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,10 @@
13
13
  "require": "./dist/index.js"
14
14
  }
15
15
  },
16
- "files": ["dist", "README.md"],
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
17
20
  "scripts": {
18
21
  "build": "tsup",
19
22
  "dev": "tsup --watch",