@pearpages/heatmap 0.3.0 → 0.4.0

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 CHANGED
@@ -1,3 +1,5 @@
1
+ [![@pearpages/heatmap — a GitHub-style contribution heatmap for React](https://heatmap.pearpages.com/og.png)](https://heatmap.pearpages.com)
2
+
1
3
  [![npm version](https://img.shields.io/npm/v/@pearpages/heatmap.svg)](https://www.npmjs.com/package/@pearpages/heatmap)
2
4
  [![Build Status](https://github.com/pearpages/heatmap/actions/workflows/publish.yml/badge.svg)](https://github.com/pearpages/heatmap/actions)
3
5
  [![License](https://img.shields.io/github/license/pearpages/heatmap.svg)](LICENSE)
@@ -5,9 +7,11 @@
5
7
 
6
8
  # Heatmap
7
9
 
8
- React component that "copies" the style of _Github_ for showing heatmaps.
10
+ A GitHub-style contribution heatmap as a React component. Themeable through CSS custom
11
+ properties, localised through `Intl`, dark-mode ready, and correct in every time zone.
12
+ No runtime dependencies: 3.3 kB gzipped, plus 1.9 kB of CSS.
9
13
 
10
- [Demo](https://heatmap.pearpages.com)
14
+ **[Live demo](https://heatmap.pearpages.com)**
11
15
 
12
16
  ## Installation
13
17
 
@@ -40,8 +44,8 @@ import {
40
44
  import '@pearpages/heatmap/styles.css';
41
45
 
42
46
  const period: Period = {
43
- start: new Date('2024-01-01'),
44
- end: new Date('2024-12-31'),
47
+ start: new Date(2024, 0, 1),
48
+ end: new Date(2024, 11, 31),
45
49
  };
46
50
 
47
51
  const contribution: ContributionData[] = [
@@ -96,6 +100,27 @@ Two period helpers are exported for the common cases:
96
100
  import { getLastYearPeriod, getLastMonthPeriod } from '@pearpages/heatmap';
97
101
  ```
98
102
 
103
+ Both run to today and start the day after the same date one year or one month earlier,
104
+ clamped to the end of a shorter month — from 31 March the month window is 1–31 March.
105
+
106
+ ### Dates are local calendar days
107
+
108
+ The library reads a `Period` boundary as the calendar day it falls on **in the local time
109
+ zone**, and every `YYYY-MM-DD` string in `ContributionData` names a local day too. Build
110
+ boundaries with the local constructor, `new Date(2024, 0, 1)`, or with the helpers above.
111
+ A date-only ISO string such as `new Date('2024-01-01')` parses as **UTC** midnight, which
112
+ is still 31 December anywhere west of Greenwich, so the first day of your period would
113
+ vanish for those users.
114
+
115
+ Two helpers convert between the two forms without going through UTC:
116
+
117
+ ```tsx
118
+ import { createDateString, parseDateString } from '@pearpages/heatmap';
119
+
120
+ createDateString(new Date(2024, 0, 5)); // '2024-01-05'
121
+ parseDateString('2024-01-05'); // local midnight, 5 Jan 2024
122
+ ```
123
+
99
124
  To try the component out without wiring up real data:
100
125
 
101
126
  ```tsx
@@ -115,7 +140,7 @@ June–August, and the occasional spike. `false` is uniform random.
115
140
  | Prop | Type | Default | Notes |
116
141
  | --- | --- | --- | --- |
117
142
  | `data.contribution` | `ContributionData[]` | required | Flat, one entry per day |
118
- | `data.period` | `Period` | required | `{ start: Date; end: Date }`. Drives the month headers and whether a tooltip reads as in-range |
143
+ | `data.period` | `Period` | required | `{ start: Date; end: Date }`, read as local calendar days. Drives the month headers and whether a tooltip reads as in-range |
119
144
  | `data.weeks` | `Week[]` | required | 7-item tuples — use `groupByWeeks` |
120
145
  | `className` | `string` | `''` | Appended to the root element; this is how themes and the colour scheme are applied |
121
146
  | `isReverse` | `boolean` | `false` | Vertical layout: one row per week, weekdays as columns |
@@ -147,19 +172,22 @@ Four palettes ship with the library. Apply one through `className`:
147
172
  | Sunset | `contribution-heatmap--sunset` |
148
173
  | Purple | `contribution-heatmap--purple` |
149
174
 
150
- Themes are nothing more than overrides of the `--color-level-0` … `--color-level-4`
151
- custom properties, so you can define your own the same way:
175
+ Themes are nothing more than overrides of the `--contribution-heatmap-level-0` …
176
+ `--contribution-heatmap-level-4` custom properties, so you can define your own the same way:
152
177
 
153
178
  ```css
154
179
  .contribution-heatmap--brand {
155
- --color-level-0: #eee;
156
- --color-level-1: #cfe8ff;
157
- --color-level-2: #7cc0ff;
158
- --color-level-3: #3b93f0;
159
- --color-level-4: #0b5cad;
180
+ --contribution-heatmap-level-0: #eee;
181
+ --contribution-heatmap-level-1: #cfe8ff;
182
+ --contribution-heatmap-level-2: #7cc0ff;
183
+ --contribution-heatmap-level-3: #3b93f0;
184
+ --contribution-heatmap-level-4: #0b5cad;
160
185
  }
161
186
  ```
162
187
 
188
+ A theme set this way applies in both light and dark. For a dark variant, set the same
189
+ properties again under your own dark selector.
190
+
163
191
  ### Light and dark
164
192
 
165
193
  By default the component follows `prefers-color-scheme`. Two more classes force it:
@@ -180,23 +208,34 @@ scrollbar matches:
180
208
  />
181
209
  ```
182
210
 
183
- ### Typography and sizing
211
+ ### Custom properties
184
212
 
185
- The same mechanism covers the font and the cell size, so there is no prop for these
186
- either:
213
+ Everything else is restyled the same way, so there is no prop for any of it. Every
214
+ public property is prefixed `--contribution-heatmap-`, the component's own block name:
187
215
 
188
- | Custom property | Default | What it does |
216
+ | Custom property (`--contribution-heatmap-…`) | Default | What it does |
189
217
  | --- | --- | --- |
190
- | `--heatmap-font-family` | a system UI stack | the font for every label |
191
- | `--heatmap-label-size` | `11px` | day names, month names, legend text |
192
- | `--heatmap-day-header-font-family` | a monospace stack | the day names above the reversed layout's columns |
193
- | `--day-size` | `12px`; `20px` minimum when reversed | the side of each square in the default layout |
194
- | `--day-gap` | `2px` (`1px` ≤768px) | the space between squares |
218
+ | `level-0` … `level-4` | GitHub greens | the five contribution colours |
219
+ | `bg`, `border` | white / `#e1e4e8` | the card's background and border |
220
+ | `fg`, `fg-muted` | `#24292f` / `#656d76` | day and month names; legend text |
221
+ | `radius`, `padding` | `6px`, `20px` (`15px` ≤768px, `10px` ≤480px) | the card |
222
+ | `shadow-color` | `rgba(0, 0, 0, 0.1)` | the card's shadow |
223
+ | `font-family` | a system UI stack | the font for every label |
224
+ | `label-size` | `11px` | day names, month names, legend text |
225
+ | `day-header-font-family` | a monospace stack | the day names above the reversed layout's columns |
226
+ | `day-size` | `12px`; `20px` minimum when reversed | the side of each square in the default layout |
227
+ | `day-gap` | `2px` (`1px` ≤768px) | the space between squares |
228
+ | `day-radius` | `2px` | the corners of each square |
229
+ | `tooltip-shadow` | a soft drop shadow | the shadow under a hovered square |
230
+ | `focus-ring` | `level-3` | the keyboard focus outline |
231
+ | `transition` | `all 0.2s ease` | hover transitions |
232
+
233
+ The colours above are the light defaults; dark mode swaps them for GitHub's dark palette.
195
234
 
196
235
  ```css
197
236
  .contribution-heatmap {
198
- --heatmap-font-family: "Inter", sans-serif;
199
- --day-size: 16px;
237
+ --contribution-heatmap-font-family: "Inter", sans-serif;
238
+ --contribution-heatmap-day-size: 16px;
200
239
  }
201
240
  ```
202
241
 
@@ -215,7 +254,7 @@ only thing in the card.
215
254
 
216
255
  The squares keep their size on small screens rather than shrinking. A year is 53 weeks, so
217
256
  the grid overflows a phone whatever size they take — it scrolls either way, and shrinking
218
- would only cost legibility and tap area. Set `--day-size` yourself if you want a denser
257
+ would only cost legibility and tap area. Set `--contribution-heatmap-day-size` yourself if you want a denser
219
258
  grid.
220
259
 
221
260
  With `isReverse`, the root also carries `contribution-heatmap--reverse`. That layout puts
@@ -229,8 +268,8 @@ a monospace one renders all seven at an identical width — in a proportional fa
229
268
 
230
269
  Because the column can be no narrower than its header, and that width depends on the
231
270
  locale — `Wed` is 20px, French `mer.` is 26.5px — the reversed square **fills its cell**
232
- rather than taking a fixed size. `--day-size` is only a floor there. That is what keeps the
233
- squares flush in any language. Override `--heatmap-day-header-font-family` under that
271
+ rather than taking a fixed size. `--contribution-heatmap-day-size` is only a floor there. That is what keeps the
272
+ squares flush in any language. Override `--contribution-heatmap-day-header-font-family` under that
234
273
  class to change the face.
235
274
 
236
275
  The grid always renders whole weeks, so the first and last week can hold days outside your
@@ -261,6 +300,51 @@ weeks Monday to Sunday, so a translated heatmap that still starts on Sunday read
261
300
  wrong. It defaults to `0` (Sunday, the GitHub convention), and the component picks the
262
301
  order of its day labels up from the data.
263
302
 
303
+ ## What's new in 0.4.0
304
+
305
+ **Every custom property now carries the component's prefix, `--contribution-heatmap-`.**
306
+ The old names were generic (`--color-bg`, `--day-size`, `--border-radius`) and could
307
+ collide with anything else on the page. The old names still work in 0.4.0 and are
308
+ removed in 0.5.0:
309
+
310
+ | Old | New |
311
+ | --- | --- |
312
+ | `--color-level-0` … `--color-level-4` | `--contribution-heatmap-level-0` … `-level-4` |
313
+ | `--color-bg` | `--contribution-heatmap-bg` |
314
+ | `--color-border` | `--contribution-heatmap-border` |
315
+ | `--color-text` | `--contribution-heatmap-fg` |
316
+ | `--color-text-muted` | `--contribution-heatmap-fg-muted` |
317
+ | `--border-radius` | `--contribution-heatmap-radius` |
318
+ | `--shadow-color` | `--contribution-heatmap-shadow-color` |
319
+ | `--transition` | `--contribution-heatmap-transition` |
320
+ | `--day-size` | `--contribution-heatmap-day-size` |
321
+ | `--day-gap` | `--contribution-heatmap-day-gap` |
322
+ | `--heatmap-font-family` | `--contribution-heatmap-font-family` |
323
+ | `--heatmap-label-size` | `--contribution-heatmap-label-size` |
324
+ | `--heatmap-day-header-font-family` | `--contribution-heatmap-day-header-font-family` |
325
+
326
+ **Four values that were fixed are now properties:** `padding`, `day-radius`,
327
+ `tooltip-shadow` and `focus-ring`. A design system can now reach every visual value.
328
+
329
+ To migrate, rename the properties you set. One difference: a theme written with the old
330
+ level names is replaced by the dark palette in dark mode, as before. A theme written with
331
+ the new names applies in both schemes.
332
+
333
+ ## What's new in 0.3.1
334
+
335
+ **Dates are read as local calendar days.** In any time zone other than UTC, 0.3.0 could
336
+ shift the first or last day of a period by one: in Europe the period started a day early
337
+ and the grid grew an extra padding week, and west of Greenwich the days read back a day
338
+ early and the week start flipped to Monday. Every `YYYY-MM-DD` string and every `Period`
339
+ boundary is now a local calendar day. `createDateString` now uses local components, and
340
+ the new `parseDateString` reads a string back as local midnight. See
341
+ [Dates are local calendar days](#dates-are-local-calendar-days).
342
+
343
+ **The package page links the demo,** the README leads with a picture, and a link to the
344
+ demo shows a proper card instead of a bare URL.
345
+
346
+ Nothing to migrate. If you worked around the shifted days yourself, remove the workaround.
347
+
264
348
  ## Local development
265
349
 
266
350
  ```bash
@@ -270,6 +354,7 @@ npm run build # build dist/
270
354
  npm test -- --run # run the test suite once
271
355
  npm run lint
272
356
  npm run check:package # publint + arethetypeswrong
357
+ npm run og -w demo # regenerate the link-preview card, demo/public/og.png (build first)
273
358
  ```
274
359
 
275
360
  `demo/` is a single Vite app that serves as both the development sandbox and the site
package/dist/index.css CHANGED
@@ -1,20 +1,5 @@
1
1
  /* src/ContributionHeatmap/index.scss */
2
2
  .contribution-heatmap {
3
- --heatmap-font-family:
4
- -apple-system,
5
- BlinkMacSystemFont,
6
- "Segoe UI",
7
- "Noto Sans",
8
- Helvetica,
9
- Arial,
10
- sans-serif;
11
- --heatmap-label-size: 11px;
12
- --heatmap-day-header-font-family:
13
- ui-monospace,
14
- SFMono-Regular,
15
- Menlo,
16
- Consolas,
17
- monospace;
18
3
  --day-size: 12px;
19
4
  --day-gap: 2px;
20
5
  --border-radius: 6px;
@@ -29,6 +14,27 @@
29
14
  --color-level-2: #40c463;
30
15
  --color-level-3: #30a14e;
31
16
  --color-level-4: #216e39;
17
+ --contribution-heatmap-bg: var(--color-bg);
18
+ --contribution-heatmap-border: var(--color-border);
19
+ --contribution-heatmap-fg: var(--color-text);
20
+ --contribution-heatmap-fg-muted: var(--color-text-muted);
21
+ --contribution-heatmap-level-0: var(--color-level-0);
22
+ --contribution-heatmap-level-1: var(--color-level-1);
23
+ --contribution-heatmap-level-2: var(--color-level-2);
24
+ --contribution-heatmap-level-3: var(--color-level-3);
25
+ --contribution-heatmap-level-4: var(--color-level-4);
26
+ --contribution-heatmap-radius: var(--border-radius);
27
+ --contribution-heatmap-shadow-color: var(--shadow-color);
28
+ --contribution-heatmap-transition: var(--transition);
29
+ --contribution-heatmap-day-size: var(--day-size);
30
+ --contribution-heatmap-day-gap: var(--day-gap);
31
+ --contribution-heatmap-font-family: var( --heatmap-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif );
32
+ --contribution-heatmap-label-size: var(--heatmap-label-size, 11px);
33
+ --contribution-heatmap-day-header-font-family: var( --heatmap-day-header-font-family, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace );
34
+ --contribution-heatmap-padding: 20px;
35
+ --contribution-heatmap-day-radius: 2px;
36
+ --contribution-heatmap-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
37
+ --contribution-heatmap-focus-ring: var(--contribution-heatmap-level-3);
32
38
  color-scheme: light dark;
33
39
  }
34
40
  @media (prefers-color-scheme: dark) {
@@ -60,16 +66,16 @@
60
66
  color-scheme: light;
61
67
  }
62
68
  .contribution-heatmap {
63
- font-family: var(--heatmap-font-family);
64
- font-size: var(--heatmap-label-size);
69
+ font-family: var(--contribution-heatmap-font-family);
70
+ font-size: var(--contribution-heatmap-label-size);
65
71
  line-height: 1;
66
- color: var(--color-text-muted);
67
- background: var(--color-bg);
68
- border-radius: var(--border-radius);
69
- padding: 20px;
70
- box-shadow: 0 2px 8px var(--shadow-color);
71
- border: 1px solid var(--color-border);
72
- transition: var(--transition);
72
+ color: var(--contribution-heatmap-fg-muted);
73
+ background: var(--contribution-heatmap-bg);
74
+ border-radius: var(--contribution-heatmap-radius);
75
+ padding: var(--contribution-heatmap-padding);
76
+ box-shadow: 0 2px 8px var(--contribution-heatmap-shadow-color);
77
+ border: 1px solid var(--contribution-heatmap-border);
78
+ transition: var(--contribution-heatmap-transition);
73
79
  box-sizing: border-box;
74
80
  max-width: 100%;
75
81
  }
@@ -82,15 +88,15 @@
82
88
  .contribution-heatmap__table {
83
89
  width: max-content;
84
90
  border-collapse: separate;
85
- border-spacing: var(--day-gap);
91
+ border-spacing: var(--contribution-heatmap-day-gap);
86
92
  }
87
93
  .contribution-heatmap__month-header,
88
94
  .contribution-heatmap__day-header,
89
95
  .contribution-heatmap__day-label,
90
96
  .contribution-heatmap__month-label {
91
- font-size: var(--heatmap-label-size);
97
+ font-size: var(--contribution-heatmap-label-size);
92
98
  font-weight: 500;
93
- color: var(--color-text);
99
+ color: var(--contribution-heatmap-fg);
94
100
  white-space: nowrap;
95
101
  padding: 0;
96
102
  border: none;
@@ -103,7 +109,7 @@
103
109
  }
104
110
  .contribution-heatmap__month-header {
105
111
  position: relative;
106
- height: calc(var(--heatmap-label-size) + 6px);
112
+ height: calc(var(--contribution-heatmap-label-size) + 6px);
107
113
  }
108
114
  .contribution-heatmap__month-header-text {
109
115
  position: absolute;
@@ -122,7 +128,7 @@
122
128
  position: sticky;
123
129
  left: 0;
124
130
  z-index: 1;
125
- background: var(--color-bg);
131
+ background: var(--contribution-heatmap-bg);
126
132
  }
127
133
  .contribution-heatmap__day-label {
128
134
  vertical-align: middle;
@@ -131,25 +137,25 @@
131
137
  vertical-align: top;
132
138
  }
133
139
  .contribution-heatmap:hover {
134
- box-shadow: 0 4px 16px var(--shadow-color);
140
+ box-shadow: 0 4px 16px var(--contribution-heatmap-shadow-color);
135
141
  }
136
142
  @media (max-width: 768px) {
137
143
  .contribution-heatmap {
138
144
  --day-gap: 1px;
139
- padding: 15px;
145
+ --contribution-heatmap-padding: 15px;
140
146
  }
141
147
  }
142
148
  @media (max-width: 480px) {
143
149
  .contribution-heatmap {
144
150
  --day-gap: 1px;
145
- padding: 10px;
151
+ --contribution-heatmap-padding: 10px;
146
152
  }
147
153
  }
148
154
  .contribution-heatmap--reverse {
149
155
  --day-size: 20px;
150
156
  }
151
157
  .contribution-heatmap--reverse .contribution-heatmap__day-header {
152
- font-family: var(--heatmap-day-header-font-family);
158
+ font-family: var(--contribution-heatmap-day-header-font-family);
153
159
  text-align: center;
154
160
  }
155
161
  .contribution-heatmap--reverse .contribution-heatmap__day {
@@ -166,9 +172,9 @@
166
172
  }
167
173
  }
168
174
  .contribution-heatmap__day {
169
- width: var(--day-size);
170
- min-width: var(--day-size);
171
- height: var(--day-size);
175
+ width: var(--contribution-heatmap-day-size);
176
+ min-width: var(--contribution-heatmap-day-size);
177
+ height: var(--contribution-heatmap-day-size);
172
178
  padding: 0;
173
179
  border: none;
174
180
  cursor: pointer;
@@ -177,54 +183,54 @@
177
183
  .contribution-heatmap__day::before {
178
184
  content: "";
179
185
  display: block;
180
- width: var(--day-size);
181
- height: var(--day-size);
186
+ width: var(--contribution-heatmap-day-size);
187
+ height: var(--contribution-heatmap-day-size);
182
188
  margin-inline: auto;
183
189
  box-sizing: border-box;
184
190
  border: 1px solid transparent;
185
- border-radius: 2px;
186
- background-color: var(--color-level-0);
187
- transition: var(--transition);
191
+ border-radius: var(--contribution-heatmap-day-radius);
192
+ background-color: var(--contribution-heatmap-level-0);
193
+ transition: var(--contribution-heatmap-transition);
188
194
  }
189
195
  .contribution-heatmap__day--level-0:hover::before {
190
- background-color: var(--color-level-1);
191
- border-color: var(--color-level-2);
196
+ background-color: var(--contribution-heatmap-level-1);
197
+ border-color: var(--contribution-heatmap-level-2);
192
198
  }
193
199
  .contribution-heatmap__day--level-1::before {
194
- background-color: var(--color-level-1);
200
+ background-color: var(--contribution-heatmap-level-1);
195
201
  }
196
202
  .contribution-heatmap__day--level-1:hover::before {
197
- background-color: var(--color-level-2);
198
- border-color: var(--color-level-3);
203
+ background-color: var(--contribution-heatmap-level-2);
204
+ border-color: var(--contribution-heatmap-level-3);
199
205
  }
200
206
  .contribution-heatmap__day--level-2::before {
201
- background-color: var(--color-level-2);
207
+ background-color: var(--contribution-heatmap-level-2);
202
208
  }
203
209
  .contribution-heatmap__day--level-2:hover::before {
204
- background-color: var(--color-level-3);
205
- border-color: var(--color-level-4);
210
+ background-color: var(--contribution-heatmap-level-3);
211
+ border-color: var(--contribution-heatmap-level-4);
206
212
  }
207
213
  .contribution-heatmap__day--level-3::before {
208
- background-color: var(--color-level-3);
214
+ background-color: var(--contribution-heatmap-level-3);
209
215
  }
210
216
  .contribution-heatmap__day--level-3:hover::before {
211
- background-color: var(--color-level-4);
212
- border-color: var(--color-level-4);
217
+ background-color: var(--contribution-heatmap-level-4);
218
+ border-color: var(--contribution-heatmap-level-4);
213
219
  }
214
220
  .contribution-heatmap__day--level-4::before {
215
- background-color: var(--color-level-4);
221
+ background-color: var(--contribution-heatmap-level-4);
216
222
  }
217
223
  .contribution-heatmap__day--level-4:hover::before {
218
224
  filter: brightness(1.1);
219
- border-color: var(--color-level-4);
225
+ border-color: var(--contribution-heatmap-level-4);
220
226
  }
221
227
  .contribution-heatmap__day:hover {
222
228
  z-index: 10;
223
229
  }
224
230
  .contribution-heatmap__day:hover::before {
225
231
  transform: scale(1.2);
226
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
227
- border-radius: 3px;
232
+ box-shadow: var(--contribution-heatmap-tooltip-shadow);
233
+ border-radius: calc(var(--contribution-heatmap-day-radius) + 1px);
228
234
  }
229
235
  .contribution-heatmap__day--outside {
230
236
  cursor: default;
@@ -236,7 +242,7 @@
236
242
  outline: none;
237
243
  }
238
244
  .contribution-heatmap__day:focus-visible::before {
239
- outline: 2px solid var(--color-level-3);
245
+ outline: 2px solid var(--contribution-heatmap-focus-ring);
240
246
  outline-offset: 2px;
241
247
  }
242
248
  .contribution-heatmap__legend {
@@ -254,8 +260,8 @@
254
260
  }
255
261
  }
256
262
  .contribution-heatmap__legend-text {
257
- color: var(--color-text-muted);
258
- font-size: var(--heatmap-label-size);
263
+ color: var(--contribution-heatmap-fg-muted);
264
+ font-size: var(--contribution-heatmap-label-size);
259
265
  font-weight: 500;
260
266
  margin: 0 4px;
261
267
  }
@@ -267,10 +273,10 @@
267
273
  .contribution-heatmap__legend-item {
268
274
  width: 12px;
269
275
  height: 12px;
270
- border-radius: 2px;
276
+ border-radius: var(--contribution-heatmap-day-radius);
271
277
  cursor: help;
272
- transition: var(--transition);
273
- border: 1px solid var(--color-border);
278
+ transition: var(--contribution-heatmap-transition);
279
+ border: 1px solid var(--contribution-heatmap-border);
274
280
  }
275
281
  @media (max-width: 768px) {
276
282
  .contribution-heatmap__legend-item {
@@ -280,22 +286,22 @@
280
286
  }
281
287
  .contribution-heatmap__legend-item:hover {
282
288
  transform: scale(1.2);
283
- border-color: var(--color-text-muted);
289
+ border-color: var(--contribution-heatmap-fg-muted);
284
290
  }
285
291
  .contribution-heatmap__legend-item--level-0 {
286
- background-color: var(--color-level-0);
292
+ background-color: var(--contribution-heatmap-level-0);
287
293
  }
288
294
  .contribution-heatmap__legend-item--level-1 {
289
- background-color: var(--color-level-1);
295
+ background-color: var(--contribution-heatmap-level-1);
290
296
  }
291
297
  .contribution-heatmap__legend-item--level-2 {
292
- background-color: var(--color-level-2);
298
+ background-color: var(--contribution-heatmap-level-2);
293
299
  }
294
300
  .contribution-heatmap__legend-item--level-3 {
295
- background-color: var(--color-level-3);
301
+ background-color: var(--contribution-heatmap-level-3);
296
302
  }
297
303
  .contribution-heatmap__legend-item--level-4 {
298
- background-color: var(--color-level-4);
304
+ background-color: var(--contribution-heatmap-level-4);
299
305
  }
300
306
  @keyframes fadeInUp {
301
307
  from {
@@ -307,13 +313,15 @@
307
313
  transform: translateY(0);
308
314
  }
309
315
  }
310
- .contribution-heatmap {
311
- animation: fadeInUp 0.6s ease-out;
312
- }
313
- .contribution-heatmap__day::before {
314
- animation: fadeInUp 0.3s ease-out;
315
- animation-delay: var(--animation-delay, 0s);
316
- animation-fill-mode: both;
316
+ @media (prefers-reduced-motion: no-preference) {
317
+ .contribution-heatmap {
318
+ animation: fadeInUp 0.6s ease-out;
319
+ }
320
+ .contribution-heatmap__day::before {
321
+ animation: fadeInUp 0.3s ease-out;
322
+ animation-delay: var(--animation-delay, 0s);
323
+ animation-fill-mode: both;
324
+ }
317
325
  }
318
326
  .contribution-heatmap--ocean {
319
327
  --color-level-0: #e6f3ff;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ContributionHeatmap/index.scss"],"sourcesContent":["// ContributionHeatmap.scss\n// GitHub-style contribution heatmap with BEM methodology\n@use 'sass:meta';\n@use '../shared/typography';\n\n@mixin dark-tokens {\n --color-bg: #0d1117;\n --color-border: #30363d;\n --color-text: #f0f6fc;\n --color-text-muted: #8b949e;\n --color-level-0: #161b22;\n --color-level-1: #0e4429;\n --color-level-2: #006d32;\n --color-level-3: #26a641;\n --color-level-4: #39d353;\n}\n\n.contribution-heatmap {\n // Typography - overridable exactly like the colour tokens below\n // meta.inspect keeps the quotes that plain interpolation would strip.\n --heatmap-font-family: #{meta.inspect(typography.$font-stack)};\n --heatmap-label-size: 11px;\n --heatmap-day-header-font-family: #{meta.inspect(typography.$mono-stack)};\n\n // Component variables\n --day-size: 12px;\n --day-gap: 2px;\n --border-radius: 6px;\n --transition: all 0.2s ease;\n --shadow-color: rgba(0, 0, 0, 0.1);\n\n // Color scheme - GitHub-inspired\n --color-bg: #ffffff;\n --color-border: #e1e4e8;\n --color-text: #24292f;\n --color-text-muted: #656d76;\n --color-level-0: #ebedf0;\n --color-level-1: #9be9a8;\n --color-level-2: #40c463;\n --color-level-3: #30a14e;\n --color-level-4: #216e39;\n\n // Dark mode. Follows the system by default; --light and --dark force it either way,\n // which is why the palette is a mixin - it has to be emitted twice, once guarded by\n // :not(--light) so forcing light wins under a dark system, and once unguarded on\n // --dark so forcing dark wins under a light one.\n color-scheme: light dark;\n\n @media (prefers-color-scheme: dark) {\n &:not(.contribution-heatmap--light) {\n @include dark-tokens;\n }\n }\n\n &--dark {\n @include dark-tokens;\n color-scheme: dark;\n }\n\n &--light {\n color-scheme: light;\n }\n\n font-family: var(--heatmap-font-family);\n font-size: var(--heatmap-label-size);\n line-height: 1;\n color: var(--color-text-muted);\n background: var(--color-bg);\n border-radius: var(--border-radius);\n padding: 20px;\n box-shadow: 0 2px 8px var(--shadow-color);\n border: 1px solid var(--color-border);\n transition: var(--transition);\n // An ordinary full-width block. It used to shrink-wrap its grid so the legend stayed\n // with it, but WebKit would not do that by any means tried - width: fit-content,\n // -webkit-fit-content, and CSS 2.1 inline-block all render full width on iOS while\n // Chrome shrink-wraps all three. The common factor is the scroll container child\n // (__scroll), though that was never confirmed. Rather than guess a fourth time, the\n // legend is aligned to the start instead, so a full-width card reads correctly\n // whatever the reason - and every browser now renders the same thing.\n //\n // border-box matters here: the library ships no reset, and under content-box the\n // padding is added on top of max-width and the card overflows its parent.\n box-sizing: border-box;\n max-width: 100%;\n\n // Only the grid scrolls - the padding, border and legend stay put.\n &__scroll {\n overflow-x: auto;\n overscroll-behavior-x: contain;\n scrollbar-width: thin;\n // overflow-x:auto makes overflow-y compute to auto too, which would clip the\n // :hover scale on the top and bottom rows. Give it room.\n padding-block: 3px;\n }\n\n // Intrinsic width: the columns are sized by the cells, never stretched to the\n // container. This is what makes --day-size mean something and lets the grid\n // scroll instead of squashing.\n &__table {\n width: max-content;\n border-collapse: separate;\n border-spacing: var(--day-gap);\n }\n\n // Sun/Mon/... and the month names. The two classes swap roles between the\n // default and reversed layouts, so both need the same treatment.\n &__month-header,\n &__day-header,\n &__day-label,\n &__month-label {\n font-size: var(--heatmap-label-size);\n font-weight: 500;\n color: var(--color-text);\n white-space: nowrap;\n padding: 0;\n border: none;\n background: none;\n }\n\n &__month-header,\n &__day-header {\n text-align: left;\n padding-bottom: 4px;\n }\n\n // A month at the edge of the period can span a single week, and its own label is\n // wider than one column. Taking the text out of flow stops it setting the column\n // width - otherwise that one week stretches and opens a gap in the grid.\n &__month-header {\n position: relative;\n height: calc(var(--heatmap-label-size) + 6px);\n }\n\n &__month-header-text {\n position: absolute;\n left: 0;\n bottom: 4px;\n }\n\n // The trailing month is the one that can span a single week, and out of flow its\n // label would overhang the last column - widening the scroll area for the sake of\n // a few pixels of text. Anchor it to the right so it overhangs inwards instead.\n &__month-header--last &__month-header-text {\n left: auto;\n right: 0;\n }\n\n // Shrink-to-fit, so the label column keeps its own width instead of\n // collapsing to a day column. Pinned, so it survives a horizontal scroll -\n // the background has to be opaque because cells slide underneath.\n &__day-label,\n &__month-label {\n width: 1%;\n text-align: right;\n padding-right: 6px;\n position: sticky;\n left: 0;\n z-index: 1;\n background: var(--color-bg);\n }\n\n &__day-label {\n vertical-align: middle;\n }\n\n // Sits against a rowSpan in the reversed layout.\n &__month-label {\n vertical-align: top;\n }\n\n // Hover effect for the entire component\n &:hover {\n box-shadow: 0 4px 16px var(--shadow-color);\n }\n\n // Mobile responsiveness. --day-size deliberately does NOT shrink here: a year is\n // 53 weeks plus a label column, so the grid overflows a phone at every size it\n // could reasonably take (507px at 8px squares, against ~373px of usable width).\n // It scrolls either way, so shrinking only cost legibility and left a 9px tap\n // target on cells that are role=\"button\". Reclaiming the padding is free, and so\n // is the tighter gap - border-spacing is not part of a cell's hit area.\n @media (max-width: 768px) {\n --day-gap: 1px;\n padding: 15px;\n }\n\n @media (max-width: 480px) {\n --day-gap: 1px;\n padding: 10px;\n }\n\n // The reversed layout puts the day names above the columns, so no column can be\n // narrower than \"Wed\". Grow the squares to match instead of leaving every one of\n // them floating in a wider cell. Only seven columns, so it fits at any width.\n &--reverse {\n // Sized to clear the widest day header, which the monospace face below brings\n // down to 19.87px from the proportional \"Wed\" at 23.7px.\n --day-size: 20px;\n\n // The day names sit above their own column here, rather than being row labels,\n // so their differing widths show up as uneven spacing. Monospace makes all\n // seven identical - if a fallback face renders wider, every column widens by\n // the same amount, so the rhythm still can't go uneven.\n .contribution-heatmap__day-header {\n font-family: var(--heatmap-day-header-font-family);\n text-align: center;\n }\n\n // The column is only ever as narrow as its day-name header, and that width is\n // locale-dependent: \"Wed\" is three characters, French \"mer.\" is four. Fill the\n // cell rather than sitting at a fixed px size, so the square tracks whatever the\n // header needs and never leaves slack. --day-size above is the floor.\n .contribution-heatmap__day {\n height: auto;\n\n &::before {\n width: 100%;\n height: auto;\n aspect-ratio: 1;\n }\n }\n\n // On a phone the month grid is the only thing in the card, so let it fill the\n // width - 20px squares marooned in a full-width card look lost. The square\n // already fills its cell above, so widening the table is all that is needed.\n // Auto layout, so __month-label's width: 1% still collapses the Aug/Sep column;\n // table-layout: fixed would divide the width equally and hand the label a full\n // day column.\n @media (max-width: 480px) {\n .contribution-heatmap__table {\n width: 100%;\n }\n }\n }\n\n // Individual day. The cell is only a slot: the visible square is drawn by\n // ::before so that a column wider than --day-size - the reversed layout sizes\n // its columns to the Sun/Mon/... headers - can never stretch the square.\n &__day {\n width: var(--day-size);\n min-width: var(--day-size);\n height: var(--day-size);\n padding: 0;\n border: none;\n cursor: pointer;\n position: relative;\n\n &::before {\n content: '';\n display: block;\n width: var(--day-size);\n height: var(--day-size);\n margin-inline: auto;\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: 2px;\n background-color: var(--color-level-0);\n transition: var(--transition);\n }\n\n // Contribution levels\n &--level-0:hover::before {\n background-color: var(--color-level-1);\n border-color: var(--color-level-2);\n }\n\n &--level-1::before {\n background-color: var(--color-level-1);\n }\n\n &--level-1:hover::before {\n background-color: var(--color-level-2);\n border-color: var(--color-level-3);\n }\n\n &--level-2::before {\n background-color: var(--color-level-2);\n }\n\n &--level-2:hover::before {\n background-color: var(--color-level-3);\n border-color: var(--color-level-4);\n }\n\n &--level-3::before {\n background-color: var(--color-level-3);\n }\n\n &--level-3:hover::before {\n background-color: var(--color-level-4);\n border-color: var(--color-level-4);\n }\n\n &--level-4::before {\n background-color: var(--color-level-4);\n }\n\n &--level-4:hover::before {\n filter: brightness(1.1);\n border-color: var(--color-level-4);\n }\n\n // Hover effects\n &:hover {\n z-index: 10;\n }\n\n &:hover::before {\n transform: scale(1.2);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n border-radius: 3px;\n }\n\n // Padding days from groupByWeeks' whole-week fill: outside the period, so\n // they hold the column open but draw nothing. visibility rather than display\n // keeps their contribution to the column width.\n &--outside {\n cursor: default;\n\n &::before {\n visibility: hidden;\n }\n }\n\n // Focus styles for accessibility\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::before {\n outline: 2px solid var(--color-level-3);\n outline-offset: 2px;\n }\n }\n\n // Legend\n &__legend {\n display: flex;\n align-items: center;\n // Start-aligned, not centred: the card is as wide as its container while the grid\n // is only as wide as its content, so centring floats the legend away from the\n // squares it describes.\n justify-content: flex-start;\n gap: 4px;\n margin-top: 12px;\n flex-wrap: wrap;\n\n @media (max-width: 768px) {\n gap: 3px;\n margin-top: 8px;\n }\n }\n\n &__legend-text {\n color: var(--color-text-muted);\n font-size: var(--heatmap-label-size);\n font-weight: 500;\n margin: 0 4px;\n\n @media (max-width: 768px) {\n margin: 0 2px;\n }\n }\n\n &__legend-item {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n cursor: help;\n transition: var(--transition);\n border: 1px solid var(--color-border);\n\n @media (max-width: 768px) {\n width: 10px;\n height: 10px;\n }\n\n &:hover {\n transform: scale(1.2);\n border-color: var(--color-text-muted);\n }\n\n &--level-0 {\n background-color: var(--color-level-0);\n }\n &--level-1 {\n background-color: var(--color-level-1);\n }\n &--level-2 {\n background-color: var(--color-level-2);\n }\n &--level-3 {\n background-color: var(--color-level-3);\n }\n &--level-4 {\n background-color: var(--color-level-4);\n }\n }\n}\n\n// Animation for initial load\n@keyframes fadeInUp {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.contribution-heatmap {\n animation: fadeInUp 0.6s ease-out;\n\n // --animation-delay is set inline per cell and inherits into the pseudo-element.\n &__day::before {\n animation: fadeInUp 0.3s ease-out;\n animation-delay: var(--animation-delay, 0s);\n animation-fill-mode: both;\n }\n}\n\n// Utilities for different themes\n.contribution-heatmap {\n // Ocean theme\n &--ocean {\n --color-level-0: #e6f3ff;\n --color-level-1: #b3daff;\n --color-level-2: #66b3ff;\n --color-level-3: #1a8cff;\n --color-level-4: #0066cc;\n }\n\n // Sunset theme\n &--sunset {\n --color-level-0: #fff2e6;\n --color-level-1: #ffcc99;\n --color-level-2: #ff9933;\n --color-level-3: #ff6600;\n --color-level-4: #cc3300;\n }\n\n // Purple theme\n &--purple {\n --color-level-0: #f3e6ff;\n --color-level-1: #d9b3ff;\n --color-level-2: #b366ff;\n --color-level-3: #8a1aff;\n --color-level-4: #6600cc;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/ContributionHeatmap/index.scss"],"sourcesContent":["// ContributionHeatmap.scss\n// GitHub-style contribution heatmap with BEM methodology\n@use 'sass:meta';\n@use '../shared/typography';\n\n@mixin dark-tokens {\n --color-bg: #0d1117;\n --color-border: #30363d;\n --color-text: #f0f6fc;\n --color-text-muted: #8b949e;\n --color-level-0: #161b22;\n --color-level-1: #0e4429;\n --color-level-2: #006d32;\n --color-level-3: #26a641;\n --color-level-4: #39d353;\n}\n\n.contribution-heatmap {\n // The public styling surface is --contribution-heatmap-*, one prefix owned by this\n // block (ADR-0012). Every rule below reads only those names.\n //\n // Deprecated in 0.4.0, removed in 0.5.0: the un-prefixed names (--color-level-N,\n // --color-bg, --day-size, ...). Until then they hold the defaults - here, in the\n // dark-tokens mixin and in the themes - and each public name is an alias of its old\n // one, so a consumer class that still sets an old name keeps today's precedence\n // exactly, dark mode included. Setting a public name replaces the alias.\n --day-size: 12px;\n --day-gap: 2px;\n --border-radius: 6px;\n --transition: all 0.2s ease;\n --shadow-color: rgba(0, 0, 0, 0.1);\n\n // Color scheme - GitHub-inspired\n --color-bg: #ffffff;\n --color-border: #e1e4e8;\n --color-text: #24292f;\n --color-text-muted: #656d76;\n --color-level-0: #ebedf0;\n --color-level-1: #9be9a8;\n --color-level-2: #40c463;\n --color-level-3: #30a14e;\n --color-level-4: #216e39;\n\n // The public names.\n --contribution-heatmap-bg: var(--color-bg);\n --contribution-heatmap-border: var(--color-border);\n --contribution-heatmap-fg: var(--color-text);\n --contribution-heatmap-fg-muted: var(--color-text-muted);\n --contribution-heatmap-level-0: var(--color-level-0);\n --contribution-heatmap-level-1: var(--color-level-1);\n --contribution-heatmap-level-2: var(--color-level-2);\n --contribution-heatmap-level-3: var(--color-level-3);\n --contribution-heatmap-level-4: var(--color-level-4);\n --contribution-heatmap-radius: var(--border-radius);\n --contribution-heatmap-shadow-color: var(--shadow-color);\n --contribution-heatmap-transition: var(--transition);\n --contribution-heatmap-day-size: var(--day-size);\n --contribution-heatmap-day-gap: var(--day-gap);\n\n // The old typography names are only read as fallbacks, never declared here: a\n // declaration on this element would shadow an inherited --heatmap-* value, and a\n // design system that wraps this component (pulp's Heatmap) names its own tokens\n // exactly that. Fonts have no dark variant, so there is no precedence to keep.\n // meta.inspect keeps the quotes that plain interpolation would strip.\n --contribution-heatmap-font-family: var(\n --heatmap-font-family,\n #{meta.inspect(typography.$font-stack)}\n );\n --contribution-heatmap-label-size: var(--heatmap-label-size, 11px);\n --contribution-heatmap-day-header-font-family: var(\n --heatmap-day-header-font-family,\n #{meta.inspect(typography.$mono-stack)}\n );\n\n // New in 0.4.0: values that used to be literals.\n --contribution-heatmap-padding: 20px;\n --contribution-heatmap-day-radius: 2px;\n --contribution-heatmap-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n --contribution-heatmap-focus-ring: var(--contribution-heatmap-level-3);\n\n // Dark mode. Follows the system by default; --light and --dark force it either way,\n // which is why the palette is a mixin - it has to be emitted twice, once guarded by\n // :not(--light) so forcing light wins under a dark system, and once unguarded on\n // --dark so forcing dark wins under a light one.\n color-scheme: light dark;\n\n @media (prefers-color-scheme: dark) {\n &:not(.contribution-heatmap--light) {\n @include dark-tokens;\n }\n }\n\n &--dark {\n @include dark-tokens;\n color-scheme: dark;\n }\n\n &--light {\n color-scheme: light;\n }\n\n font-family: var(--contribution-heatmap-font-family);\n font-size: var(--contribution-heatmap-label-size);\n line-height: 1;\n color: var(--contribution-heatmap-fg-muted);\n background: var(--contribution-heatmap-bg);\n border-radius: var(--contribution-heatmap-radius);\n padding: var(--contribution-heatmap-padding);\n box-shadow: 0 2px 8px var(--contribution-heatmap-shadow-color);\n border: 1px solid var(--contribution-heatmap-border);\n transition: var(--contribution-heatmap-transition);\n // An ordinary full-width block. It used to shrink-wrap its grid so the legend stayed\n // with it, but WebKit would not do that by any means tried - width: fit-content,\n // -webkit-fit-content, and CSS 2.1 inline-block all render full width on iOS while\n // Chrome shrink-wraps all three. The common factor is the scroll container child\n // (__scroll), though that was never confirmed. Rather than guess a fourth time, the\n // legend is aligned to the start instead, so a full-width card reads correctly\n // whatever the reason - and every browser now renders the same thing.\n //\n // border-box matters here: the library ships no reset, and under content-box the\n // padding is added on top of max-width and the card overflows its parent.\n box-sizing: border-box;\n max-width: 100%;\n\n // Only the grid scrolls - the padding, border and legend stay put.\n &__scroll {\n overflow-x: auto;\n overscroll-behavior-x: contain;\n scrollbar-width: thin;\n // overflow-x:auto makes overflow-y compute to auto too, which would clip the\n // :hover scale on the top and bottom rows. Give it room.\n padding-block: 3px;\n }\n\n // Intrinsic width: the columns are sized by the cells, never stretched to the\n // container. This is what makes --day-size mean something and lets the grid\n // scroll instead of squashing.\n &__table {\n width: max-content;\n border-collapse: separate;\n border-spacing: var(--contribution-heatmap-day-gap);\n }\n\n // Sun/Mon/... and the month names. The two classes swap roles between the\n // default and reversed layouts, so both need the same treatment.\n &__month-header,\n &__day-header,\n &__day-label,\n &__month-label {\n font-size: var(--contribution-heatmap-label-size);\n font-weight: 500;\n color: var(--contribution-heatmap-fg);\n white-space: nowrap;\n padding: 0;\n border: none;\n background: none;\n }\n\n &__month-header,\n &__day-header {\n text-align: left;\n padding-bottom: 4px;\n }\n\n // A month at the edge of the period can span a single week, and its own label is\n // wider than one column. Taking the text out of flow stops it setting the column\n // width - otherwise that one week stretches and opens a gap in the grid.\n &__month-header {\n position: relative;\n height: calc(var(--contribution-heatmap-label-size) + 6px);\n }\n\n &__month-header-text {\n position: absolute;\n left: 0;\n bottom: 4px;\n }\n\n // The trailing month is the one that can span a single week, and out of flow its\n // label would overhang the last column - widening the scroll area for the sake of\n // a few pixels of text. Anchor it to the right so it overhangs inwards instead.\n &__month-header--last &__month-header-text {\n left: auto;\n right: 0;\n }\n\n // Shrink-to-fit, so the label column keeps its own width instead of\n // collapsing to a day column. Pinned, so it survives a horizontal scroll -\n // the background has to be opaque because cells slide underneath.\n &__day-label,\n &__month-label {\n width: 1%;\n text-align: right;\n padding-right: 6px;\n position: sticky;\n left: 0;\n z-index: 1;\n background: var(--contribution-heatmap-bg);\n }\n\n &__day-label {\n vertical-align: middle;\n }\n\n // Sits against a rowSpan in the reversed layout.\n &__month-label {\n vertical-align: top;\n }\n\n // Hover effect for the entire component\n &:hover {\n box-shadow: 0 4px 16px var(--contribution-heatmap-shadow-color);\n }\n\n // Mobile responsiveness. --day-size deliberately does NOT shrink here: a year is\n // 53 weeks plus a label column, so the grid overflows a phone at every size it\n // could reasonably take (507px at 8px squares, against ~373px of usable width).\n // It scrolls either way, so shrinking only cost legibility and left a 9px tap\n // target on cells that are role=\"button\". Reclaiming the padding is free, and so\n // is the tighter gap - border-spacing is not part of a cell's hit area.\n @media (max-width: 768px) {\n --day-gap: 1px;\n --contribution-heatmap-padding: 15px;\n }\n\n @media (max-width: 480px) {\n --day-gap: 1px;\n --contribution-heatmap-padding: 10px;\n }\n\n // The reversed layout puts the day names above the columns, so no column can be\n // narrower than \"Wed\". Grow the squares to match instead of leaving every one of\n // them floating in a wider cell. Only seven columns, so it fits at any width.\n &--reverse {\n // Sized to clear the widest day header, which the monospace face below brings\n // down to 19.87px from the proportional \"Wed\" at 23.7px.\n --day-size: 20px;\n\n // The day names sit above their own column here, rather than being row labels,\n // so their differing widths show up as uneven spacing. Monospace makes all\n // seven identical - if a fallback face renders wider, every column widens by\n // the same amount, so the rhythm still can't go uneven.\n .contribution-heatmap__day-header {\n font-family: var(--contribution-heatmap-day-header-font-family);\n text-align: center;\n }\n\n // The column is only ever as narrow as its day-name header, and that width is\n // locale-dependent: \"Wed\" is three characters, French \"mer.\" is four. Fill the\n // cell rather than sitting at a fixed px size, so the square tracks whatever the\n // header needs and never leaves slack. --day-size above is the floor.\n .contribution-heatmap__day {\n height: auto;\n\n &::before {\n width: 100%;\n height: auto;\n aspect-ratio: 1;\n }\n }\n\n // On a phone the month grid is the only thing in the card, so let it fill the\n // width - 20px squares marooned in a full-width card look lost. The square\n // already fills its cell above, so widening the table is all that is needed.\n // Auto layout, so __month-label's width: 1% still collapses the Aug/Sep column;\n // table-layout: fixed would divide the width equally and hand the label a full\n // day column.\n @media (max-width: 480px) {\n .contribution-heatmap__table {\n width: 100%;\n }\n }\n }\n\n // Individual day. The cell is only a slot: the visible square is drawn by\n // ::before so that a column wider than --day-size - the reversed layout sizes\n // its columns to the Sun/Mon/... headers - can never stretch the square.\n &__day {\n width: var(--contribution-heatmap-day-size);\n min-width: var(--contribution-heatmap-day-size);\n height: var(--contribution-heatmap-day-size);\n padding: 0;\n border: none;\n cursor: pointer;\n position: relative;\n\n &::before {\n content: '';\n display: block;\n width: var(--contribution-heatmap-day-size);\n height: var(--contribution-heatmap-day-size);\n margin-inline: auto;\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: var(--contribution-heatmap-day-radius);\n background-color: var(--contribution-heatmap-level-0);\n transition: var(--contribution-heatmap-transition);\n }\n\n // Contribution levels\n &--level-0:hover::before {\n background-color: var(--contribution-heatmap-level-1);\n border-color: var(--contribution-heatmap-level-2);\n }\n\n &--level-1::before {\n background-color: var(--contribution-heatmap-level-1);\n }\n\n &--level-1:hover::before {\n background-color: var(--contribution-heatmap-level-2);\n border-color: var(--contribution-heatmap-level-3);\n }\n\n &--level-2::before {\n background-color: var(--contribution-heatmap-level-2);\n }\n\n &--level-2:hover::before {\n background-color: var(--contribution-heatmap-level-3);\n border-color: var(--contribution-heatmap-level-4);\n }\n\n &--level-3::before {\n background-color: var(--contribution-heatmap-level-3);\n }\n\n &--level-3:hover::before {\n background-color: var(--contribution-heatmap-level-4);\n border-color: var(--contribution-heatmap-level-4);\n }\n\n &--level-4::before {\n background-color: var(--contribution-heatmap-level-4);\n }\n\n &--level-4:hover::before {\n filter: brightness(1.1);\n border-color: var(--contribution-heatmap-level-4);\n }\n\n // Hover effects\n &:hover {\n z-index: 10;\n }\n\n &:hover::before {\n transform: scale(1.2);\n box-shadow: var(--contribution-heatmap-tooltip-shadow);\n border-radius: calc(var(--contribution-heatmap-day-radius) + 1px);\n }\n\n // Padding days from groupByWeeks' whole-week fill: outside the period, so\n // they hold the column open but draw nothing. visibility rather than display\n // keeps their contribution to the column width.\n &--outside {\n cursor: default;\n\n &::before {\n visibility: hidden;\n }\n }\n\n // Focus styles for accessibility\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::before {\n outline: 2px solid var(--contribution-heatmap-focus-ring);\n outline-offset: 2px;\n }\n }\n\n // Legend\n &__legend {\n display: flex;\n align-items: center;\n // Start-aligned, not centred: the card is as wide as its container while the grid\n // is only as wide as its content, so centring floats the legend away from the\n // squares it describes.\n justify-content: flex-start;\n gap: 4px;\n margin-top: 12px;\n flex-wrap: wrap;\n\n @media (max-width: 768px) {\n gap: 3px;\n margin-top: 8px;\n }\n }\n\n &__legend-text {\n color: var(--contribution-heatmap-fg-muted);\n font-size: var(--contribution-heatmap-label-size);\n font-weight: 500;\n margin: 0 4px;\n\n @media (max-width: 768px) {\n margin: 0 2px;\n }\n }\n\n &__legend-item {\n width: 12px;\n height: 12px;\n border-radius: var(--contribution-heatmap-day-radius);\n cursor: help;\n transition: var(--contribution-heatmap-transition);\n border: 1px solid var(--contribution-heatmap-border);\n\n @media (max-width: 768px) {\n width: 10px;\n height: 10px;\n }\n\n &:hover {\n transform: scale(1.2);\n border-color: var(--contribution-heatmap-fg-muted);\n }\n\n &--level-0 {\n background-color: var(--contribution-heatmap-level-0);\n }\n &--level-1 {\n background-color: var(--contribution-heatmap-level-1);\n }\n &--level-2 {\n background-color: var(--contribution-heatmap-level-2);\n }\n &--level-3 {\n background-color: var(--contribution-heatmap-level-3);\n }\n &--level-4 {\n background-color: var(--contribution-heatmap-level-4);\n }\n }\n}\n\n// Animation for initial load\n@keyframes fadeInUp {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n// A year is 371 staggered cells plus the card itself; none of it runs for users who\n// have asked their OS for less motion. The cells have no `from` state outside the\n// animation, so with it off they simply appear in place.\n@media (prefers-reduced-motion: no-preference) {\n .contribution-heatmap {\n animation: fadeInUp 0.6s ease-out;\n\n // --animation-delay is set inline per cell and inherits into the pseudo-element.\n &__day::before {\n animation: fadeInUp 0.3s ease-out;\n animation-delay: var(--animation-delay, 0s);\n animation-fill-mode: both;\n }\n }\n}\n\n// Utilities for different themes. They still write the deprecated names in 0.4.0: moved\n// to the public ones, they would start applying in dark mode, where today the\n// dark-tokens mixin (higher specificity) replaces them.\n.contribution-heatmap {\n // Ocean theme\n &--ocean {\n --color-level-0: #e6f3ff;\n --color-level-1: #b3daff;\n --color-level-2: #66b3ff;\n --color-level-3: #1a8cff;\n --color-level-4: #0066cc;\n }\n\n // Sunset theme\n &--sunset {\n --color-level-0: #fff2e6;\n --color-level-1: #ffcc99;\n --color-level-2: #ff9933;\n --color-level-3: #ff6600;\n --color-level-4: #cc3300;\n }\n\n // Purple theme\n &--purple {\n --color-level-0: #f3e6ff;\n --color-level-1: #d9b3ff;\n --color-level-2: #b366ff;\n --color-level-3: #8a1aff;\n --color-level-4: #6600cc;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ declare const dayNames: readonly ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat
31
31
  declare function getLastYearPeriod(): Period;
32
32
  declare function getLastMonthPeriod(): Period;
33
33
  declare const createDateString: (date: Date) => string;
34
+ declare const parseDateString: (dateString: string) => Date;
34
35
 
35
36
  interface ContributionHeatmapProps {
36
37
  data: {
@@ -55,4 +56,4 @@ declare function generateMockData({ period, isRealistic, }: {
55
56
  isRealistic: boolean;
56
57
  }): ContributionData[];
57
58
 
58
- export { type ContributionData, ContributionHeatmap, type GroupByWeeksOptions, type HeatmapLabels, type Period, type Theme, type Week, createDateString, dayNames, generateMockData, getLastMonthPeriod, getLastYearPeriod, groupByWeeks, monthNames };
59
+ export { type ContributionData, ContributionHeatmap, type GroupByWeeksOptions, type HeatmapLabels, type Period, type Theme, type Week, createDateString, dayNames, generateMockData, getLastMonthPeriod, getLastYearPeriod, groupByWeeks, monthNames, parseDateString };
package/dist/index.js CHANGED
@@ -24,24 +24,52 @@ var monthNames = [
24
24
  "Dec"
25
25
  ];
26
26
  var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
27
+ function shiftMonthsClamped(date, months) {
28
+ const year = date.getFullYear();
29
+ const month = date.getMonth() + months;
30
+ const daysInTarget = new Date(year, month + 1, 0).getDate();
31
+ return new Date(year, month, Math.min(date.getDate(), daysInTarget));
32
+ }
33
+ var startOfToday = () => {
34
+ const now = /* @__PURE__ */ new Date();
35
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
36
+ };
27
37
  function getLastYearPeriod() {
28
- const start = new Date(
29
- (/* @__PURE__ */ new Date()).setFullYear(
30
- (/* @__PURE__ */ new Date()).getFullYear() - 1,
31
- (/* @__PURE__ */ new Date()).getMonth(),
32
- (/* @__PURE__ */ new Date()).getDate() + 1
33
- )
34
- );
35
- const end = /* @__PURE__ */ new Date();
38
+ const end = startOfToday();
39
+ const start = shiftMonthsClamped(end, -12);
40
+ start.setDate(start.getDate() + 1);
36
41
  return { start, end };
37
42
  }
38
43
  function getLastMonthPeriod() {
39
- const now = /* @__PURE__ */ new Date();
40
- const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
41
- const start = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate() + 1);
44
+ const end = startOfToday();
45
+ const start = shiftMonthsClamped(end, -1);
46
+ start.setDate(start.getDate() + 1);
42
47
  return { start, end };
43
48
  }
44
- var createDateString = (date) => date.toISOString().split("T")[0];
49
+ var pad = (n) => String(n).padStart(2, "0");
50
+ var createDateString = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
51
+ var parseDateString = (dateString) => {
52
+ const [year, month, day] = dateString.split("-").map(Number);
53
+ return new Date(year, month - 1, day);
54
+ };
55
+
56
+ // src/shared/formatTooltip.tsx
57
+ function isInRange(date, { start, end }) {
58
+ return date >= createDateString(start) && date <= createDateString(end);
59
+ }
60
+ var formatTooltip = (contribution, { start, end }, { locale = DEFAULT_LOCALE, labels } = {}) => {
61
+ const text = { ...defaultLabels, ...labels };
62
+ const formattedDate = parseDateString(contribution.date).toLocaleDateString(locale, {
63
+ weekday: "short",
64
+ year: "numeric",
65
+ month: "short",
66
+ day: "numeric"
67
+ });
68
+ if (!isInRange(contribution.date, { start, end }) || contribution.count === 0) {
69
+ return `${formattedDate}: ${text.noContributions}`;
70
+ }
71
+ return `${formattedDate}: ${text.contributions(contribution.count)}`;
72
+ };
45
73
 
46
74
  // src/ContributionHeatmap/getMonthsForHeader.ts
47
75
  function getMonthsForHeader({
@@ -50,24 +78,20 @@ function getMonthsForHeader({
50
78
  monthNames: monthNames2 = monthNames
51
79
  }) {
52
80
  const months = [];
53
- const { start, end } = period;
54
81
  let currentMonth = -1;
55
82
  let currentSpan = 0;
56
83
  weeks.forEach((week, weekIndex) => {
57
84
  if (week.length > 0) {
58
85
  let monthToUse = -1;
59
- const actualStartDate = new Date(start);
60
- const actualEndDate = new Date(end);
61
86
  for (const day of week) {
62
- const dayDate = new Date(day.date);
63
- if (dayDate >= actualStartDate && dayDate <= actualEndDate) {
64
- monthToUse = dayDate.getMonth();
87
+ if (isInRange(day.date, period)) {
88
+ monthToUse = parseDateString(day.date).getMonth();
65
89
  break;
66
90
  }
67
91
  }
68
92
  if (monthToUse === -1) {
69
93
  const middleDay = week[3];
70
- monthToUse = new Date(middleDay.date).getMonth();
94
+ monthToUse = parseDateString(middleDay.date).getMonth();
71
95
  }
72
96
  if (monthToUse !== currentMonth) {
73
97
  if (currentMonth !== -1) {
@@ -86,26 +110,6 @@ function getMonthsForHeader({
86
110
  return months;
87
111
  }
88
112
 
89
- // src/shared/formatTooltip.tsx
90
- function isInRange(date, { start, end }) {
91
- return date >= createDateString(start) && date <= createDateString(end);
92
- }
93
- var formatTooltip = (contribution, { start, end }, { locale = DEFAULT_LOCALE, labels } = {}) => {
94
- const text = { ...defaultLabels, ...labels };
95
- const date = new Date(contribution.date);
96
- const formattedDate = date.toLocaleDateString(locale, {
97
- weekday: "short",
98
- year: "numeric",
99
- month: "short",
100
- day: "numeric",
101
- timeZone: "UTC"
102
- });
103
- if (!isInRange(contribution.date, { start, end }) || contribution.count === 0) {
104
- return `${formattedDate}: ${text.noContributions}`;
105
- }
106
- return `${formattedDate}: ${text.contributions(contribution.count)}`;
107
- };
108
-
109
113
  // src/shared/intl.ts
110
114
  var REFERENCE_WEEK_START = Date.UTC(2024, 0, 7);
111
115
  var DAYS_IN_WEEK = 7;
@@ -172,7 +176,7 @@ function ContributionHeatmap({
172
176
  locale = DEFAULT_LOCALE,
173
177
  labels
174
178
  }) {
175
- const weekStartsOn = weeks.length ? new Date(weeks[0][0].date).getUTCDay() : 0;
179
+ const weekStartsOn = weeks.length ? parseDateString(weeks[0][0].date).getDay() : 0;
176
180
  const dayNames2 = getDayNames(locale, weekStartsOn);
177
181
  const months = getMonthsForHeader({ weeks, period, monthNames: getMonthNames(locale) });
178
182
  return /* @__PURE__ */ jsxs(
@@ -289,9 +293,9 @@ var createWeek = (startDate, contributionMap) => {
289
293
  var groupByWeeks = (contributions, { weekStartsOn = 0 } = {}) => {
290
294
  const weeks = [];
291
295
  const contributionMap = createContributionMap(contributions);
292
- const firstDay = getFirstDayOfWeek(new Date(contributions[0].date), weekStartsOn);
296
+ const firstDay = getFirstDayOfWeek(parseDateString(contributions[0].date), weekStartsOn);
293
297
  const lastDate = getLastDayOfWeek(
294
- new Date(contributions[contributions.length - 1].date),
298
+ parseDateString(contributions[contributions.length - 1].date),
295
299
  weekStartsOn
296
300
  );
297
301
  const currentDate = new Date(firstDay);
@@ -362,6 +366,6 @@ function generateMockData({
362
366
  return data;
363
367
  }
364
368
 
365
- export { ContributionHeatmap, createDateString, dayNames, generateMockData, getLastMonthPeriod, getLastYearPeriod, groupByWeeks, monthNames };
369
+ export { ContributionHeatmap, createDateString, dayNames, generateMockData, getLastMonthPeriod, getLastYearPeriod, groupByWeeks, monthNames, parseDateString };
366
370
  //# sourceMappingURL=index.js.map
367
371
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared/models.tsx","../src/ContributionHeatmap/getMonthsForHeader.ts","../src/shared/formatTooltip.tsx","../src/shared/intl.ts","../src/shared/Legend.tsx","../src/ContributionHeatmap/index.tsx","../src/shared/groupByWeeks.ts","../src/mocks/createRealisticMockedCount.ts","../src/mocks/index.tsx"],"names":["monthNames","jsx","dayNames","jsxs"],"mappings":";;;AA8BA,IAAM,aAAA,GAAyC;AAAA,EAC7C,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,CAAC,KAAA,KAAU,CAAA,MAAA,EAAS,KAAK,CAAA,CAAA;AAAA,EAChC,eAAA,EAAiB,kBAAA;AAAA,EACjB,aAAA,EAAe,CAAC,KAAA,KAAU,CAAA,EAAG,KAAK,CAAA,aAAA,EAAgB,KAAA,KAAU,CAAA,GAAI,GAAA,GAAM,EAAE,CAAA;AAC1E,CAAA;AAEA,IAAM,cAAA,GAAiB,OAAA;AAEvB,IAAM,UAAA,GAAa;AAAA,EACjB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF;AACA,IAAM,QAAA,GAAW,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK;AAEjE,SAAS,iBAAA,GAA4B;AACnC,EAAA,MAAM,QAAQ,IAAI,IAAA;AAAA,IAAA,iBAChB,IAAI,MAAK,EAAE,WAAA;AAAA,MAAA,iBACT,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY,GAAI,CAAA;AAAA,MAAA,iBAC3B,IAAI,IAAA,EAAK,EAAE,QAAA,EAAS;AAAA,MAAA,iBACpB,IAAI,IAAA,EAAK,EAAE,OAAA,EAAQ,GAAI;AAAA;AACzB,GACF;AACA,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,OAAO,EAAE,OAAO,GAAA,EAAI;AACtB;AAEA,SAAS,kBAAA,GAA6B;AACpC,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,GAAA,GAAM,IAAI,IAAA,CAAK,GAAA,CAAI,WAAA,EAAY,EAAG,GAAA,CAAI,QAAA,EAAS,EAAG,GAAA,CAAI,OAAA,EAAS,CAAA;AACrE,EAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,GAAA,CAAI,WAAA,EAAY,EAAG,GAAA,CAAI,QAAA,EAAS,GAAI,CAAA,EAAG,GAAA,CAAI,OAAA,KAAY,CAAC,CAAA;AAC/E,EAAA,OAAO,EAAE,OAAO,GAAA,EAAI;AACtB;AAEA,IAAM,gBAAA,GAAmB,CAAC,IAAA,KACxB,IAAA,CAAK,aAAY,CAAE,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC;;;AC1EjC,SAAS,kBAAA,CAAmB;AAAA,EAC1B,KAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAAA,WAAAA,GAAa;AACf,CAAA,EASI;AACF,EAAA,MAAM,SAA0D,EAAC;AACjE,EAAA,MAAM,EAAE,KAAA,EAAO,GAAA,EAAI,GAAI,MAAA;AACvB,EAAA,IAAI,YAAA,GAAe,EAAA;AACnB,EAAA,IAAI,WAAA,GAAc,CAAA;AAElB,EAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,SAAA,KAAc;AACjC,IAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AAEnB,MAAA,IAAI,UAAA,GAAa,EAAA;AACjB,MAAA,MAAM,eAAA,GAAkB,IAAI,IAAA,CAAK,KAAK,CAAA;AACtC,MAAA,MAAM,aAAA,GAAgB,IAAI,IAAA,CAAK,GAAG,CAAA;AAElC,MAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,QAAA,MAAM,OAAA,GAAU,IAAI,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AACjC,QAAA,IAAI,OAAA,IAAW,eAAA,IAAmB,OAAA,IAAW,aAAA,EAAe;AAC1D,UAAA,UAAA,GAAa,QAAQ,QAAA,EAAS;AAC9B,UAAA;AAAA,QACF;AAAA,MACF;AAIA,MAAA,IAAI,eAAe,EAAA,EAAI;AACrB,QAAA,MAAM,SAAA,GAAY,KAAK,CAAC,CAAA;AACxB,QAAA,UAAA,GAAa,IAAI,IAAA,CAAK,SAAA,CAAU,IAAI,EAAE,QAAA,EAAS;AAAA,MACjD;AAEA,MAAA,IAAI,eAAe,YAAA,EAAc;AAC/B,QAAA,IAAI,iBAAiB,EAAA,EAAI;AACvB,UAAA,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAMA,WAAAA,CAAW,YAAY,CAAA,EAAG,IAAA,EAAM,WAAA,EAAa,KAAA,EAAO,SAAA,GAAY,WAAA,EAAa,CAAA;AAAA,QACnG;AACA,QAAA,YAAA,GAAe,UAAA;AACf,QAAA,WAAA,GAAc,CAAA;AAAA,MAChB,CAAA,MAAO;AACL,QAAA,WAAA,EAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AAED,EAAA,IAAI,iBAAiB,EAAA,EAAI;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK,EAAE,IAAA,EAAMA,WAAAA,CAAW,YAAY,CAAA,EAAG,IAAA,EAAM,WAAA,EAAa,KAAA,EAAO,KAAA,CAAM,MAAA,GAAS,WAAA,EAAa,CAAA;AAAA,EACtG;AAEA,EAAA,OAAO,MAAA;AACT;;;AC/CA,SAAS,SAAA,CAAU,IAAA,EAAc,EAAE,KAAA,EAAO,KAAI,EAAoB;AAChE,EAAA,OAAO,QAAQ,gBAAA,CAAiB,KAAK,CAAA,IAAK,IAAA,IAAQ,iBAAiB,GAAG,CAAA;AACxE;AAOA,IAAM,aAAA,GAAgB,CACpB,YAAA,EACA,EAAE,KAAA,EAAO,GAAA,EAAI,EACb,EAAE,MAAA,GAAS,cAAA,EAAgB,MAAA,EAAO,GAA0B,EAAC,KAClD;AACX,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,aAAA,EAAe,GAAG,MAAA,EAAO;AAC3C,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,YAAA,CAAa,IAAI,CAAA;AAGvC,EAAA,MAAM,aAAA,GAAgB,IAAA,CAAK,kBAAA,CAAmB,MAAA,EAAQ;AAAA,IACpD,OAAA,EAAS,OAAA;AAAA,IACT,IAAA,EAAM,SAAA;AAAA,IACN,KAAA,EAAO,OAAA;AAAA,IACP,GAAA,EAAK,SAAA;AAAA,IACL,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,IAAI,CAAC,SAAA,CAAU,YAAA,CAAa,IAAA,EAAM,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA,IAAK,YAAA,CAAa,KAAA,KAAU,CAAA,EAAG;AAC7E,IAAA,OAAO,CAAA,EAAG,aAAa,CAAA,EAAA,EAAK,IAAA,CAAK,eAAe,CAAA,CAAA;AAAA,EAClD;AAEA,EAAA,OAAO,GAAG,aAAa,CAAA,EAAA,EAAK,KAAK,aAAA,CAAc,YAAA,CAAa,KAAK,CAAC,CAAA,CAAA;AACpE,CAAA;;;ACrCA,IAAM,oBAAA,GAAuB,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,GAAG,CAAC,CAAA;AAChD,IAAM,YAAA,GAAe,CAAA;AAErB,SAAS,WAAA,CAAY,MAAA,EAAgB,YAAA,GAAuB,CAAA,EAAa;AACvE,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ;AAAA,IAC7C,OAAA,EAAS,OAAA;AAAA,IACT,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,OAAO,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,cAAa,EAAG,CAAC,GAAG,KAAA,KAAU;AACxD,IAAA,MAAM,SAAA,GAAA,CAAa,eAAe,KAAA,IAAS,YAAA;AAC3C,IAAA,OAAO,OAAO,MAAA,CAAO,IAAI,KAAK,oBAAA,GAAuB,SAAA,GAAY,KAAQ,CAAC,CAAA;AAAA,EAC5E,CAAC,CAAA;AACH;AAEA,SAAS,cAAc,MAAA,EAA0B;AAC/C,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ;AAAA,IAC7C,KAAA,EAAO,OAAA;AAAA,IACP,QAAA,EAAU;AAAA,GACX,CAAA;AAGD,EAAA,OAAO,KAAA,CAAM,IAAA;AAAA,IAAK,EAAE,QAAQ,EAAA,EAAG;AAAA,IAAG,CAAC,CAAA,EAAG,KAAA,KACpC,MAAA,CAAO,MAAA,CAAO,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,KAAA,EAAO,EAAE,CAAC,CAAC;AAAA,GACnD;AACF;AC9BA,SAAS,MAAA,CAAO,EAAE,MAAA,EAAO,EAAgD;AACvE,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,aAAA,EAAe,GAAG,MAAA,EAAO;AAE3C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8BAAA,EACb,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mCAAA,EAAqC,QAAA,EAAA,IAAA,CAAK,IAAA,EAAK,CAAA;AAAA,IAC9D,CAAC,GAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,KAAA,qBACpB,GAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QAEC,SAAA,EAAW,8EAA8E,KAAK,CAAA,CAAA;AAAA,QAC9F,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,KAAK;AAAA,OAAA;AAAA,MAFlB;AAAA,KAIR,CAAA;AAAA,oBACD,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,mCAAA,EAAqC,eAAK,IAAA,EAAK;AAAA,GAAA,EACjE,CAAA;AAEJ;ACQA,SAAS,QAAQ,EAAE,YAAA,EAAc,QAAQ,MAAA,EAAQ,MAAA,EAAQ,YAAW,EAAiB;AACnF,EAAA,MAAM,OAAA,GAAU,SAAA,CAAU,YAAA,CAAa,IAAA,EAAM,MAAM,CAAA;AACnD,EAAA,MAAM,UAAU,aAAA,CAAc,YAAA,EAAc,QAAQ,EAAE,MAAA,EAAQ,QAAQ,CAAA;AAEtE,EAAA,uBACEC,GAAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW;AAAA,QACT,2BAAA;AAAA,QACA,CAAA,iCAAA,EAAoC,aAAa,KAAK,CAAA,CAAA;AAAA,QACtD,UAAU,EAAA,GAAK;AAAA,OACjB,CACG,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAAA,MACX,KAAA,EAAO,UAAU,OAAA,GAAU,MAAA;AAAA,MAC3B,YAAA,EAAY,UAAU,OAAA,GAAU,MAAA;AAAA,MAChC,IAAA,EAAM,UAAU,QAAA,GAAW,MAAA;AAAA,MAC3B,QAAA,EAAU,UAAU,CAAA,GAAI,MAAA;AAAA,MACxB,cAAY,YAAA,CAAa,KAAA;AAAA,MACzB,aAAW,YAAA,CAAa,IAAA;AAAA,MACxB,OAAO,EAAE,mBAAA,EAAqB,CAAA,EAAG,UAAA,GAAa,IAAK,CAAA,CAAA,CAAA;AAAI;AAAA,GACxD;AAEL;AAYA,SAAS,mBAAA,CAAoB;AAAA,EAC3B,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAA,EAAM;AAAA,EACtB,SAAA,GAAY,EAAA;AAAA,EACZ,SAAA,GAAY,KAAA;AAAA,EACZ,MAAA,GAAS,cAAA;AAAA,EACT;AACF,CAAA,EAA6B;AAG3B,EAAA,MAAM,YAAA,GAAe,KAAA,CAAM,MAAA,GAAS,IAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAE,CAAC,CAAA,CAAE,IAAI,CAAA,CAAE,WAAU,GAAI,CAAA;AAC7E,EAAA,MAAMC,SAAAA,GAAW,WAAA,CAAY,MAAA,EAAQ,YAAY,CAAA;AACjD,EAAA,MAAM,MAAA,GAAS,mBAAmB,EAAE,KAAA,EAAO,QAAQ,UAAA,EAAY,aAAA,CAAc,MAAM,CAAA,EAAG,CAAA;AAEtF,EAAA,uBACEC,IAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,WAAW,CAAA,oBAAA,EAAuB,SAAA,GAAY,gCAAA,GAAmC,EAAE,IAAI,SAAS,CAAA,CAAA;AAAA,MAGhG,QAAA,EAAA;AAAA,wBAAAF,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,8BAAA,EACb,QAAA,kBAAAA,GAAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,6BAAA,EACd,QAAA,EAAA,SAAA,mBACCE,IAAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,0BAAAF,GAAAA,CAAC,OAAA,EAAA,EACC,QAAA,kBAAAE,IAAAA,CAAC,IAAA,EAAA,EACC,QAAA,EAAA;AAAA,4BAAAF,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,oCAAA,EAAqC,CAAA;AAAA,YAClDC,SAAAA,CAAS,GAAA,CAAI,CAAC,OAAA,EAAS,QAAA,qBACtBD,GAAAA,CAAC,IAAA,EAAA,EAAkB,SAAA,EAAU,kCAAA,EAC1B,QAAA,EAAA,OAAA,EAAA,EADM,QAET,CACD;AAAA,WAAA,EACH,CAAA,EACF,CAAA;AAAA,0BACAA,GAAAA,CAAC,OAAA,EAAA,EACE,gBAAM,GAAA,CAAI,CAAC,MAAM,SAAA,KAAc;AAE9B,YAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,CAAK,CAAA,CAAA,KAAK,SAAA,KAAc,EAAE,KAAK,CAAA;AACpD,YAAA,MAAM,cAAA,GAAiB,CAAC,CAAC,KAAA;AACzB,YAAA,uBACEE,KAAC,IAAA,EAAA,EACE,QAAA,EAAA;AAAA,cAAA,cAAA,oBACCF,GAAAA;AAAA,gBAAC,IAAA;AAAA,gBAAA;AAAA,kBACC,SAAA,EAAU,mCAAA;AAAA,kBACV,SAAS,KAAA,CAAM,IAAA;AAAA,kBAEd,QAAA,EAAA,KAAA,CAAM;AAAA;AAAA,eACT;AAAA,cAED,IAAA,CAAK,GAAA,CAAI,CAAC,YAAA,EAAc,6BACvBA,GAAAA;AAAA,gBAAC,OAAA;AAAA,gBAAA;AAAA,kBAEC,YAAA;AAAA,kBACA,MAAA;AAAA,kBACA,MAAA;AAAA,kBACA,MAAA;AAAA,kBACA,UAAA,EAAY,YAAY,CAAA,GAAI;AAAA,iBAAA;AAAA,gBALvB;AAAA,eAOR;AAAA,aAAA,EAAA,EAlBM,SAmBT,CAAA;AAAA,UAEJ,CAAC,CAAA,EACH;AAAA,SAAA,EACF,CAAA,mBAEAE,IAAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,0BAAAF,GAAAA,CAAC,OAAA,EAAA,EACC,QAAA,kBAAAE,IAAAA,CAAC,IAAA,EAAA,EACC,QAAA,EAAA;AAAA,4BAAAF,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,kCAAA,EAAmC,CAAA;AAAA,YAChD,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,EAAO,0BAClBA,GAAAA;AAAA,cAAC,IAAA;AAAA,cAAA;AAAA,gBAEC,WAAW,CAAA,kCAAA,EACT,KAAA,KAAU,OAAO,MAAA,GAAS,CAAA,GACtB,8CACA,EACN,CAAA,CAAA;AAAA,gBACA,SAAS,KAAA,CAAM,IAAA;AAAA,gBAEf,0BAAAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,yCAAA,EACb,gBAAM,IAAA,EACT;AAAA,eAAA;AAAA,cAVK;AAAA,aAYR;AAAA,WAAA,EACH,CAAA,EACF,CAAA;AAAA,0BACAA,GAAAA,CAAC,OAAA,EAAA,EACE,QAAA,EAAAC,SAAAA,CAAS,GAAA,CAAI,CAAC,OAAA,EAAS,QAAA,qBACtBC,IAAAA,CAAC,IAAA,EAAA,EACC,QAAA,EAAA;AAAA,4BAAAF,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,iCAAA,EACX,QAAA,EAAA,OAAA,EACH,CAAA;AAAA,YACC,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,EAAM,SAAA,KAAc;AAC9B,cAAA,MAAM,YAAA,GAAe,KAAK,QAAQ,CAAA;AAClC,cAAA,uBACEA,GAAAA;AAAA,gBAAC,OAAA;AAAA,gBAAA;AAAA,kBAEC,YAAA;AAAA,kBACA,MAAA;AAAA,kBACA,MAAA;AAAA,kBACA,MAAA;AAAA,kBACA,UAAA,EAAY,YAAY,CAAA,GAAI;AAAA,iBAAA;AAAA,gBALvB;AAAA,eAMP;AAAA,YAEJ,CAAC;AAAA,WAAA,EAAA,EAhBM,QAiBT,CACD,CAAA,EACH;AAAA,SAAA,EACF,GAEJ,CAAA,EACF,CAAA;AAAA,wBACAA,GAAAA,CAAC,MAAA,EAAA,EAAO,MAAA,EAAgB;AAAA;AAAA;AAAA,GAC1B;AAEJ;;;AC3KA,IAAM,sBAAA,GAAyB,CAAC,IAAA,MAAoC;AAAA,EAClE,IAAA;AAAA,EACA,KAAA,EAAO,CAAA;AAAA,EACP,KAAA,EAAO;AACT,CAAA,CAAA;AAEA,IAAM,qBAAA,GAAwB,CAC5B,aAAA,KACkC;AAClC,EAAA,MAAM,eAAA,uBAAsB,GAAA,EAA8B;AAC1D,EAAA,aAAA,CAAc,OAAA,CAAQ,CAAC,YAAA,KAAiB;AACtC,IAAA,eAAA,CAAgB,GAAA,CAAI,YAAA,CAAa,IAAA,EAAM,YAAY,CAAA;AAAA,EACrD,CAAC,CAAA;AACD,EAAA,OAAO,eAAA;AACT,CAAA;AAIA,IAAM,YAAA,GAAe,CAAC,IAAA,EAAY,YAAA,KAAA,CAC/B,KAAK,MAAA,EAAO,GAAI,eAAe,CAAA,IAAK,CAAA;AAEvC,IAAM,iBAAA,GAAoB,CAAC,SAAA,EAAiB,YAAA,KAA+B;AACzE,EAAA,MAAM,WAAA,GAAc,IAAI,IAAA,CAAK,SAAS,CAAA;AACtC,EAAA,WAAA,CAAY,QAAQ,SAAA,CAAU,OAAA,KAAY,YAAA,CAAa,SAAA,EAAW,YAAY,CAAC,CAAA;AAC/E,EAAA,OAAO,WAAA;AACT,CAAA;AAEA,IAAM,gBAAA,GAAmB,CAAC,QAAA,EAAgB,YAAA,KAA+B;AACvE,EAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,QAAQ,CAAA;AACnC,EAAA,SAAA,CAAU,OAAA,CAAQ,SAAS,OAAA,EAAQ,IAAK,IAAI,YAAA,CAAa,QAAA,EAAU,YAAY,CAAA,CAAE,CAAA;AACjF,EAAA,OAAO,SAAA;AACT,CAAA;AAEA,IAAM,UAAA,GAAa,CACjB,SAAA,EACA,eAAA,KACS;AACT,EAAA,MAAM,OAA2B,EAAC;AAClC,EAAA,MAAM,WAAA,GAAc,IAAI,IAAA,CAAK,SAAS,CAAA;AACtC,EAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,GAAM,CAAA,EAAG,GAAA,EAAA,EAAO;AAChC,IAAA,MAAM,UAAA,GAAa,iBAAiB,WAAW,CAAA;AAC/C,IAAA,MAAM,YAAA,GAAe,eAAA,CAAgB,GAAA,CAAI,UAAU,CAAA;AAEnD,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,IAAA,CAAK,KAAK,YAAY,CAAA;AAAA,IACxB,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,IAAA,CAAK,sBAAA,CAAuB,UAAU,CAAC,CAAA;AAAA,IAC9C;AACA,IAAA,WAAA,CAAY,OAAA,CAAQ,WAAA,CAAY,OAAA,EAAQ,GAAI,CAAC,CAAA;AAAA,EAC/C;AAEA,EAAA,OAAO,IAAA;AACT,CAAA;AAOA,IAAM,YAAA,GAAe,CACnB,aAAA,EACA,EAAE,eAAe,CAAA,EAAE,GAAyB,EAAC,KAClC;AACX,EAAA,MAAM,QAAgB,EAAC;AACvB,EAAA,MAAM,eAAA,GAAkB,sBAAsB,aAAa,CAAA;AAE3D,EAAA,MAAM,QAAA,GAAW,kBAAkB,IAAI,IAAA,CAAK,cAAc,CAAC,CAAA,CAAE,IAAI,CAAA,EAAG,YAAY,CAAA;AAChF,EAAA,MAAM,QAAA,GAAW,gBAAA;AAAA,IACf,IAAI,IAAA,CAAK,aAAA,CAAc,cAAc,MAAA,GAAS,CAAC,EAAE,IAAI,CAAA;AAAA,IACrD;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAAc,IAAI,IAAA,CAAK,QAAQ,CAAA;AACrC,EAAA,OAAO,eAAe,QAAA,EAAU;AAC9B,IAAA,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,WAAA,EAAa,eAAe,CAAC,CAAA;AACnD,IAAA,WAAA,CAAY,OAAA,CAAQ,WAAA,CAAY,OAAA,EAAQ,GAAI,CAAC,CAAA;AAAA,EAC/C;AAEA,EAAA,OAAO,KAAA;AACT;;;AC/EA,IAAM,aAAA,GAAgB,CAAC,KAAA,KAAkB;AACvC,EAAA,OAAO,KAAA,IAAS,KAAK,KAAA,IAAS,CAAA;AAChC,CAAA;AAEO,SAAS,2BACd,IAAA,EAC2B;AAE3B,EAAA,MAAM,SAAA,GAAY,KAAK,MAAA,EAAO;AAC9B,EAAA,MAAM,SAAA,GAAY,SAAA,KAAc,CAAA,IAAK,SAAA,KAAc,CAAA;AACnD,EAAA,MAAM,KAAA,GAAQ,KAAK,QAAA,EAAS;AAE5B,EAAA,IAAI,SAAA,GAAY,YAAY,CAAA,GAAI,CAAA;AAGhC,EAAA,IAAI,aAAA,CAAc,KAAK,CAAA,EAAG;AACxB,IAAA,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAA,GAAY,CAAC,CAAA;AAAA,EACvC;AAGA,EAAA,MAAM,YAAA,GAAe,KAAK,MAAA,EAAO;AACjC,EAAA,IAAI,KAAA,GAAQ,CAAA;AAEZ,EAAA,IAAI,eAAe,GAAA,EAAK;AACtB,IAAA,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,SAAS,CAAA;AAAA,EAC9C;AAGA,EAAA,IAAI,eAAe,IAAA,EAAM;AACvB,IAAA,KAAA,IAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAA,KAAW,EAAE,CAAA;AAAA,EACxC;AAEA,EAAA,OAAO,KAAA;AACT;;;AChCA,IAAM,iBAAiB,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,MAAA,KAAW,EAAE,CAAA;AAE1D,IAAM,QAAA,GAAW,CAAC,KAAA,KAA6C;AAC7D,EAAA,IAAI,KAAA,GAA2B,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,GAAG,KAAA,GAAQ,CAAA;AAAA,OAAA,IAChB,KAAA,IAAS,GAAG,KAAA,GAAQ,CAAA;AAAA,OAAA,IACpB,KAAA,IAAS,GAAG,KAAA,GAAQ,CAAA;AAAA,OAAA,IACpB,KAAA,IAAS,GAAG,KAAA,GAAQ,CAAA;AAAA,OACxB,KAAA,GAAQ,CAAA;AAEb,EAAA,OAAO,KAAA;AACT,CAAA;AAEA,SAAS,SAAS,MAAA,EAA6B;AAC7C,EAAA,IAAI,WAAW,UAAA,EAAY;AACzB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,KAAA,uBAAY,IAAA,EAAK;AACvB,EAAA,KAAA,CAAM,WAAA,CAAY,KAAA,CAAM,WAAA,EAAY,GAAI,CAAC,CAAA;AACzC,EAAA,OAAO,EAAE,OAAO,GAAA,EAAI;AACtB;AAEA,SAAS,gBAAA,CAAiB;AAAA,EACxB,MAAA;AAAA,EACA;AACF,CAAA,EAGuB;AACrB,EAAA,MAAM,OAA2B,EAAC;AAClC,EAAA,MAAM,EAAE,KAAA,EAAO,GAAA,EAAI,GAAI,SAAS,MAAM,CAAA;AAEtC,EAAA,KAAA,IACM,IAAA,GAAO,IAAI,IAAA,CAAK,KAAK,CAAA,EACzB,IAAA,IAAQ,GAAA,EACR,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,OAAA,EAAQ,GAAI,CAAC,CAAA,EAC/B;AACA,IAAA,MAAM,KAAA,GAAQ,WAAA,GACV,0BAAA,CAA2B,IAAI,IAC/B,cAAA,EAAe;AACnB,IAAA,IAAA,CAAK,IAAA,CAAK;AAAA,MACR,IAAA,EAAM,iBAAiB,IAAI,CAAA;AAAA,MAC3B,KAAA;AAAA,MACA,KAAA,EAAO,SAAS,KAAK;AAAA,KACtB,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,IAAA;AACT","file":"index.js","sourcesContent":["type Theme = '' | 'ocean' | 'sunset' | 'purple';\n\ninterface ContributionData {\n date: string;\n count: number;\n level: 0 | 1 | 2 | 3 | 4;\n}\n\ntype Week = [\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n];\n\ntype Period = { start: Date; end: Date };\n\n// The strings Intl cannot derive from a locale. Day and month names are not here:\n// those come from the locale itself, via src/shared/intl.ts.\ninterface HeatmapLabels {\n less?: string;\n more?: string;\n level?: (level: number) => string;\n noContributions?: string;\n contributions?: (count: number) => string;\n}\n\nconst defaultLabels: Required<HeatmapLabels> = {\n less: 'Less',\n more: 'More',\n level: (level) => `Level ${level}`,\n noContributions: 'No contributions',\n contributions: (count) => `${count} contribution${count !== 1 ? 's' : ''}`,\n};\n\nconst DEFAULT_LOCALE = 'en-US';\n\nconst monthNames = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n] as const;\nconst dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;\n\nfunction getLastYearPeriod(): Period {\n const start = new Date(\n new Date().setFullYear(\n new Date().getFullYear() - 1,\n new Date().getMonth(),\n new Date().getDate() + 1,\n ),\n );\n const end = new Date();\n return { start, end };\n}\n\nfunction getLastMonthPeriod(): Period {\n const now = new Date();\n const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const start = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate() + 1);\n return { start, end };\n}\n\nconst createDateString = (date: Date): string =>\n date.toISOString().split('T')[0];\n\nexport type { ContributionData, Week, Period, Theme, HeatmapLabels };\nexport {\n monthNames,\n dayNames,\n defaultLabels,\n DEFAULT_LOCALE,\n createDateString,\n getLastYearPeriod,\n getLastMonthPeriod,\n};\n","import { monthNames as defaultMonthNames, type Period, type Week } from '@/shared/models';\n\nfunction getMonthsForHeader({\n weeks,\n period,\n monthNames = defaultMonthNames,\n}: {\n weeks: Week[];\n period: Period;\n // Locale-derived; falls back to the exported English constants.\n monthNames?: readonly string[];\n}): {\n name: string;\n span: number;\n start: number;\n}[] {\n const months: { name: string; span: number; start: number }[] = [];\n const { start, end } = period;\n let currentMonth = -1;\n let currentSpan = 0;\n\n weeks.forEach((week, weekIndex) => {\n if (week.length > 0) {\n // Find the first day in the week that falls within our actual data range\n let monthToUse = -1;\n const actualStartDate = new Date(start);\n const actualEndDate = new Date(end);\n\n for (const day of week) {\n const dayDate = new Date(day.date);\n if (dayDate >= actualStartDate && dayDate <= actualEndDate) {\n monthToUse = dayDate.getMonth();\n break;\n }\n }\n\n // If no day in the week is in our actual range, use the middle of the week\n // (index 3 - which weekday that is depends on weekStartsOn)\n if (monthToUse === -1) {\n const middleDay = week[3];\n monthToUse = new Date(middleDay.date).getMonth();\n }\n\n if (monthToUse !== currentMonth) {\n if (currentMonth !== -1) {\n months.push({ name: monthNames[currentMonth], span: currentSpan, start: weekIndex - currentSpan });\n }\n currentMonth = monthToUse;\n currentSpan = 1;\n } else {\n currentSpan++;\n }\n }\n });\n\n if (currentMonth !== -1) {\n months.push({ name: monthNames[currentMonth], span: currentSpan, start: weeks.length - currentSpan });\n }\n\n return months;\n}\n\nexport { getMonthsForHeader };\n","import {\n createDateString,\n defaultLabels,\n DEFAULT_LOCALE,\n type ContributionData,\n type HeatmapLabels,\n type Period,\n} from '@/shared/models';\n\n// Compares whole days, in the same YYYY-MM-DD form the contributions carry.\n// Period boundaries are Date objects that keep a time of day, so comparing them\n// directly against a date-only value put the first and last day of the period\n// out of range. ISO date strings also sort correctly, so > and < still work.\nfunction isInRange(date: string, { start, end }: Period): boolean {\n return date >= createDateString(start) && date <= createDateString(end);\n}\n\ninterface FormatTooltipOptions {\n locale?: string;\n labels?: HeatmapLabels;\n}\n\nconst formatTooltip = (\n contribution: ContributionData,\n { start, end }: Period,\n { locale = DEFAULT_LOCALE, labels }: FormatTooltipOptions = {},\n): string => {\n const text = { ...defaultLabels, ...labels };\n const date = new Date(contribution.date);\n // UTC: contribution.date is a date-only string, which parses as UTC midnight.\n // Formatting it in a local zone would name the previous day in negative offsets.\n const formattedDate = date.toLocaleDateString(locale, {\n weekday: 'short',\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n timeZone: 'UTC',\n });\n\n if (!isInRange(contribution.date, { start, end }) || contribution.count === 0) {\n return `${formattedDate}: ${text.noContributions}`;\n }\n\n return `${formattedDate}: ${text.contributions(contribution.count)}`;\n};\n\nexport { formatTooltip, isInRange };\nexport type { FormatTooltipOptions };\n","// Day and month names derived from a locale, so the component ships no translations.\n//\n// timeZone is pinned to UTC throughout: the reference dates below are built with\n// Date.UTC, and formatting them in a local zone would shift them across a day boundary\n// in negative offsets and hand back the wrong name.\n\n// 7 Jan 2024 is a Sunday, so this week runs Sunday -> Saturday.\nconst REFERENCE_WEEK_START = Date.UTC(2024, 0, 7);\nconst DAYS_IN_WEEK = 7;\n\nfunction getDayNames(locale: string, weekStartsOn: number = 0): string[] {\n const format = new Intl.DateTimeFormat(locale, {\n weekday: 'short',\n timeZone: 'UTC',\n });\n\n return Array.from({ length: DAYS_IN_WEEK }, (_, index) => {\n const dayOfWeek = (weekStartsOn + index) % DAYS_IN_WEEK;\n return format.format(new Date(REFERENCE_WEEK_START + dayOfWeek * 86400000));\n });\n}\n\nfunction getMonthNames(locale: string): string[] {\n const format = new Intl.DateTimeFormat(locale, {\n month: 'short',\n timeZone: 'UTC',\n });\n\n // The 15th avoids any month-length edge case.\n return Array.from({ length: 12 }, (_, month) =>\n format.format(new Date(Date.UTC(2024, month, 15))),\n );\n}\n\nexport { getDayNames, getMonthNames };\n","import { defaultLabels, type HeatmapLabels } from '@/shared/models';\n\nfunction Legend({ labels }: { labels?: HeatmapLabels }): React.ReactNode {\n const text = { ...defaultLabels, ...labels };\n\n return (\n <div className=\"contribution-heatmap__legend\">\n <span className=\"contribution-heatmap__legend-text\">{text.less}</span>\n {[0, 1, 2, 3, 4].map((level) => (\n <div\n key={level}\n className={`contribution-heatmap__legend-item contribution-heatmap__legend-item--level-${level}`}\n title={text.level(level)}\n ></div>\n ))}\n <span className=\"contribution-heatmap__legend-text\">{text.more}</span>\n </div>\n );\n}\n\nexport { Legend };\n","import './index.scss';\nimport type {\n ContributionData,\n HeatmapLabels,\n Period,\n Week as WeekType,\n} from '../shared/models';\nimport { getMonthsForHeader } from './getMonthsForHeader';\nimport { formatTooltip, isInRange } from '@/shared/formatTooltip';\nimport { DEFAULT_LOCALE } from '@/shared/models';\nimport { getDayNames, getMonthNames } from '@/shared/intl';\nimport { Legend } from '@/shared/Legend';\n\n\ninterface DayCellProps {\n contribution: ContributionData;\n period: Period;\n locale: string;\n labels?: HeatmapLabels;\n // Position in the grid, used to stagger the load animation.\n delayIndex: number;\n}\n\n// groupByWeeks pads every week out to Sun-Sat, so the first and last week carry days\n// that fall outside the period. They keep their slot - the columns depend on seven\n// cells per row - but render no square and stay out of the a11y tree.\nfunction DayCell({ contribution, period, locale, labels, delayIndex }: DayCellProps) {\n const inRange = isInRange(contribution.date, period);\n const tooltip = formatTooltip(contribution, period, { locale, labels });\n\n return (\n <td\n className={[\n 'contribution-heatmap__day',\n `contribution-heatmap__day--level-${contribution.level}`,\n inRange ? '' : 'contribution-heatmap__day--outside',\n ]\n .filter(Boolean)\n .join(' ')}\n title={inRange ? tooltip : undefined}\n aria-label={inRange ? tooltip : undefined}\n role={inRange ? 'button' : undefined}\n tabIndex={inRange ? 0 : undefined}\n data-count={contribution.count}\n data-date={contribution.date}\n style={{ '--animation-delay': `${delayIndex * 0.005}s` } as React.CSSProperties}\n ></td>\n );\n}\n\ninterface ContributionHeatmapProps {\n data: { contribution: ContributionData[]; period: Period; weeks: WeekType[] };\n className?: string;\n isReverse?: boolean;\n // Drives day names, month names and the tooltip date through Intl.\n locale?: string;\n // The handful of strings Intl cannot derive.\n labels?: HeatmapLabels;\n}\n\nfunction ContributionHeatmap({\n data: { period, weeks },\n className = '',\n isReverse = false,\n locale = DEFAULT_LOCALE,\n labels,\n}: ContributionHeatmapProps) {\n // Read the week start off the data rather than taking a prop: groupByWeeks already\n // decided it, and a prop could disagree with the weeks it was handed.\n const weekStartsOn = weeks.length ? new Date(weeks[0][0].date).getUTCDay() : 0;\n const dayNames = getDayNames(locale, weekStartsOn);\n const months = getMonthsForHeader({ weeks, period, monthNames: getMonthNames(locale) });\n\n return (\n <div\n className={`contribution-heatmap${isReverse ? ' contribution-heatmap--reverse' : ''} ${className}`}\n >\n {/* Only the grid scrolls; the padding and the legend stay put. */}\n <div className=\"contribution-heatmap__scroll\">\n <table className=\"contribution-heatmap__table\">\n {isReverse ? (\n <>\n <thead>\n <tr>\n <th className=\"contribution-heatmap__month-header\"></th>\n {dayNames.map((dayName, dayIndex) => (\n <th key={dayIndex} className=\"contribution-heatmap__day-header\">\n {dayName}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {weeks.map((week, weekIndex) => {\n // Find month info for this week\n const month = months.find(m => weekIndex === m.start);\n const showMonthLabel = !!month;\n return (\n <tr key={weekIndex}>\n {showMonthLabel && (\n <td\n className=\"contribution-heatmap__month-label\"\n rowSpan={month.span}\n >\n {month.name}\n </td>\n )}\n {week.map((contribution, dayIndex) => (\n <DayCell\n key={dayIndex}\n contribution={contribution}\n period={period}\n locale={locale}\n labels={labels}\n delayIndex={weekIndex * 7 + dayIndex}\n />\n ))}\n </tr>\n );\n })}\n </tbody>\n </>\n ) : (\n <>\n <thead>\n <tr>\n <th className=\"contribution-heatmap__day-header\"></th>\n {months.map((month, index) => (\n <th\n key={index}\n className={`contribution-heatmap__month-header${\n index === months.length - 1\n ? ' contribution-heatmap__month-header--last'\n : ''\n }`}\n colSpan={month.span}\n >\n <span className=\"contribution-heatmap__month-header-text\">\n {month.name}\n </span>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {dayNames.map((dayName, dayIndex) => (\n <tr key={dayIndex}>\n <td className=\"contribution-heatmap__day-label\">\n {dayName}\n </td>\n {weeks.map((week, weekIndex) => {\n const contribution = week[dayIndex];\n return (\n <DayCell\n key={weekIndex}\n contribution={contribution}\n period={period}\n locale={locale}\n labels={labels}\n delayIndex={weekIndex * 7 + dayIndex}\n />\n );\n })}\n </tr>\n ))}\n </tbody>\n </>\n )}\n </table>\n </div>\n <Legend labels={labels} />\n </div>\n );\n}\n\nexport { ContributionHeatmap };\n","import { createDateString, type ContributionData, type Week } from '@/shared/models';\n\nconst createEmpyContribution = (date: string): ContributionData => ({\n date,\n count: 0,\n level: 0,\n});\n\nconst createContributionMap = (\n contributions: ContributionData[],\n): Map<string, ContributionData> => {\n const contributionMap = new Map<string, ContributionData>();\n contributions.forEach((contribution) => {\n contributionMap.set(contribution.date, contribution);\n });\n return contributionMap;\n};\n\n// Days from weekStartsOn back to the given date's weekday, e.g. with a Monday start\n// (1) a Sunday (0) is six days into the week, not minus one.\nconst daysIntoWeek = (date: Date, weekStartsOn: number): number =>\n (date.getDay() - weekStartsOn + 7) % 7;\n\nconst getFirstDayOfWeek = (firstDate: Date, weekStartsOn: number): Date => {\n const startOfWeek = new Date(firstDate);\n startOfWeek.setDate(firstDate.getDate() - daysIntoWeek(firstDate, weekStartsOn));\n return startOfWeek;\n};\n\nconst getLastDayOfWeek = (lastDate: Date, weekStartsOn: number): Date => {\n const endOfWeek = new Date(lastDate);\n endOfWeek.setDate(lastDate.getDate() + (6 - daysIntoWeek(lastDate, weekStartsOn)));\n return endOfWeek;\n};\n\nconst createWeek = (\n startDate: Date,\n contributionMap: Map<string, ContributionData>,\n): Week => {\n const week: ContributionData[] = [];\n const currentDate = new Date(startDate);\n for (let day = 0; day < 7; day++) {\n const dateString = createDateString(currentDate);\n const contribution = contributionMap.get(dateString);\n\n if (contribution) {\n week.push(contribution);\n } else {\n week.push(createEmpyContribution(dateString));\n }\n currentDate.setDate(currentDate.getDate() + 1);\n }\n\n return week as Week;\n};\n\ninterface GroupByWeeksOptions {\n // 0 = Sunday (the default, and the US/GitHub convention), 1 = Monday, and so on.\n weekStartsOn?: number;\n}\n\nconst groupByWeeks = (\n contributions: ContributionData[],\n { weekStartsOn = 0 }: GroupByWeeksOptions = {},\n): Week[] => {\n const weeks: Week[] = [];\n const contributionMap = createContributionMap(contributions);\n\n const firstDay = getFirstDayOfWeek(new Date(contributions[0].date), weekStartsOn);\n const lastDate = getLastDayOfWeek(\n new Date(contributions[contributions.length - 1].date),\n weekStartsOn,\n );\n\n const currentDate = new Date(firstDay);\n while (currentDate <= lastDate) {\n weeks.push(createWeek(currentDate, contributionMap));\n currentDate.setDate(currentDate.getDate() + 7);\n }\n\n return weeks;\n};\n\nexport { groupByWeeks };\nexport type { GroupByWeeksOptions };\n","import type { ContributionData } from '@/shared/models';\n\nconst isSummerMonth = (month: number) => {\n return month >= 5 && month <= 7;\n};\n\nexport function createRealisticMockedCount(\n date: Date,\n): ContributionData['count'] {\n // Create realistic patterns\n const dayOfWeek = date.getDay();\n const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;\n const month = date.getMonth();\n\n let baseCount = isWeekend ? 2 : 8;\n\n // Lower activity in summer months (June, July, August)\n if (isSummerMonth(month)) {\n baseCount = Math.max(0, baseCount - 3);\n }\n\n // Add some randomness\n const randomFactor = Math.random();\n let count = 0;\n\n if (randomFactor > 0.3) {\n count = Math.floor(Math.random() * baseCount);\n }\n\n // Occasional high activity days\n if (randomFactor > 0.95) {\n count += Math.floor(Math.random() * 10);\n }\n\n return count;\n}\n","import { createDateString, type ContributionData, type Period } from '@/shared/models';\nimport { createRealisticMockedCount } from './createRealisticMockedCount';\n\nconst getRandomCount = () => Math.floor(Math.random() * 15);\n\nconst getLevel = (count: number): ContributionData['level'] => {\n let level: 0 | 1 | 2 | 3 | 4 = 0;\n if (count === 0) level = 0;\n else if (count <= 2) level = 1;\n else if (count <= 5) level = 2;\n else if (count <= 8) level = 3;\n else level = 4;\n\n return level;\n};\n\nfunction getRange(period: Period | 'lastYear') {\n if (period !== 'lastYear') {\n return period;\n }\n const end = new Date(); // Today\n const start = new Date();\n start.setFullYear(start.getFullYear() - 1); // One year ago\n return { start, end };\n}\n\nfunction generateMockData({\n period,\n isRealistic,\n}: {\n period: Period | 'lastYear';\n isRealistic: boolean;\n}): ContributionData[] {\n const data: ContributionData[] = [];\n const { start, end } = getRange(period);\n\n for (\n let date = new Date(start);\n date <= end;\n date.setDate(date.getDate() + 1)\n ) {\n const count = isRealistic\n ? createRealisticMockedCount(date)\n : getRandomCount();\n data.push({\n date: createDateString(date),\n count,\n level: getLevel(count),\n });\n }\n\n return data;\n}\n\nexport { generateMockData };\n"]}
1
+ {"version":3,"sources":["../src/shared/models.tsx","../src/shared/formatTooltip.tsx","../src/ContributionHeatmap/getMonthsForHeader.ts","../src/shared/intl.ts","../src/shared/Legend.tsx","../src/ContributionHeatmap/index.tsx","../src/shared/groupByWeeks.ts","../src/mocks/createRealisticMockedCount.ts","../src/mocks/index.tsx"],"names":["monthNames","jsx","dayNames","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["type Theme = '' | 'ocean' | 'sunset' | 'purple';\n\ninterface ContributionData {\n date: string;\n count: number;\n level: 0 | 1 | 2 | 3 | 4;\n}\n\ntype Week = [\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n ContributionData,\n];\n\ntype Period = { start: Date; end: Date };\n\n// The strings Intl cannot derive from a locale. Day and month names are not here:\n// those come from the locale itself, via src/shared/intl.ts.\ninterface HeatmapLabels {\n less?: string;\n more?: string;\n level?: (level: number) => string;\n noContributions?: string;\n contributions?: (count: number) => string;\n}\n\nconst defaultLabels: Required<HeatmapLabels> = {\n less: 'Less',\n more: 'More',\n level: (level) => `Level ${level}`,\n noContributions: 'No contributions',\n contributions: (count) => `${count} contribution${count !== 1 ? 's' : ''}`,\n};\n\nconst DEFAULT_LOCALE = 'en-US';\n\nconst monthNames = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n] as const;\nconst dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;\n\n// Local midnight, `months` whole months away, with the day-of-month clamped to the\n// target month's length: Date would otherwise roll Feb 31 forward into March.\nfunction shiftMonthsClamped(date: Date, months: number): Date {\n const year = date.getFullYear();\n const month = date.getMonth() + months;\n const daysInTarget = new Date(year, month + 1, 0).getDate();\n return new Date(year, month, Math.min(date.getDate(), daysInTarget));\n}\n\nconst startOfToday = (): Date => {\n const now = new Date();\n return new Date(now.getFullYear(), now.getMonth(), now.getDate());\n};\n\n// Both helpers span back to the day after the same date one unit ago, so today is\n// the last day and the window is exactly one year / one month long.\nfunction getLastYearPeriod(): Period {\n const end = startOfToday();\n const start = shiftMonthsClamped(end, -12);\n start.setDate(start.getDate() + 1);\n return { start, end };\n}\n\nfunction getLastMonthPeriod(): Period {\n const end = startOfToday();\n const start = shiftMonthsClamped(end, -1);\n start.setDate(start.getDate() + 1);\n return { start, end };\n}\n\n// The library reads every Date as a calendar day in the local time zone, and these two\n// are the only conversions between Date and the YYYY-MM-DD strings the data carries.\n// Neither goes through toISOString() or `new Date(string)`: both of those are UTC, and\n// mixing them with local Dates shifts the first and last day of a period by one\n// anywhere outside Greenwich.\nconst pad = (n: number): string => String(n).padStart(2, '0');\n\nconst createDateString = (date: Date): string =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\nconst parseDateString = (dateString: string): Date => {\n const [year, month, day] = dateString.split('-').map(Number);\n return new Date(year, month - 1, day);\n};\n\nexport type { ContributionData, Week, Period, Theme, HeatmapLabels };\nexport {\n monthNames,\n dayNames,\n defaultLabels,\n DEFAULT_LOCALE,\n createDateString,\n parseDateString,\n getLastYearPeriod,\n getLastMonthPeriod,\n};\n","import {\n createDateString,\n parseDateString,\n defaultLabels,\n DEFAULT_LOCALE,\n type ContributionData,\n type HeatmapLabels,\n type Period,\n} from '@/shared/models';\n\n// Compares whole days, in the same YYYY-MM-DD form the contributions carry.\n// Period boundaries are Date objects that keep a time of day, so comparing them\n// directly against a date-only value put the first and last day of the period\n// out of range. ISO date strings also sort correctly, so > and < still work.\nfunction isInRange(date: string, { start, end }: Period): boolean {\n return date >= createDateString(start) && date <= createDateString(end);\n}\n\ninterface FormatTooltipOptions {\n locale?: string;\n labels?: HeatmapLabels;\n}\n\nconst formatTooltip = (\n contribution: ContributionData,\n { start, end }: Period,\n { locale = DEFAULT_LOCALE, labels }: FormatTooltipOptions = {},\n): string => {\n const text = { ...defaultLabels, ...labels };\n // parseDateString gives local midnight, so formatting in the local zone names the\n // right day. `new Date(string)` would parse as UTC midnight, which is the previous\n // day anywhere west of Greenwich.\n const formattedDate = parseDateString(contribution.date).toLocaleDateString(locale, {\n weekday: 'short',\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n\n if (!isInRange(contribution.date, { start, end }) || contribution.count === 0) {\n return `${formattedDate}: ${text.noContributions}`;\n }\n\n return `${formattedDate}: ${text.contributions(contribution.count)}`;\n};\n\nexport { formatTooltip, isInRange };\nexport type { FormatTooltipOptions };\n","import { isInRange } from '@/shared/formatTooltip';\nimport {\n monthNames as defaultMonthNames,\n parseDateString,\n type Period,\n type Week,\n} from '@/shared/models';\n\nfunction getMonthsForHeader({\n weeks,\n period,\n monthNames = defaultMonthNames,\n}: {\n weeks: Week[];\n period: Period;\n // Locale-derived; falls back to the exported English constants.\n monthNames?: readonly string[];\n}): {\n name: string;\n span: number;\n start: number;\n}[] {\n const months: { name: string; span: number; start: number }[] = [];\n let currentMonth = -1;\n let currentSpan = 0;\n\n weeks.forEach((week, weekIndex) => {\n if (week.length > 0) {\n // Find the first day in the week that falls within our actual data range\n let monthToUse = -1;\n\n for (const day of week) {\n if (isInRange(day.date, period)) {\n monthToUse = parseDateString(day.date).getMonth();\n break;\n }\n }\n\n // If no day in the week is in our actual range, use the middle of the week\n // (index 3 - which weekday that is depends on weekStartsOn)\n if (monthToUse === -1) {\n const middleDay = week[3];\n monthToUse = parseDateString(middleDay.date).getMonth();\n }\n\n if (monthToUse !== currentMonth) {\n if (currentMonth !== -1) {\n months.push({ name: monthNames[currentMonth], span: currentSpan, start: weekIndex - currentSpan });\n }\n currentMonth = monthToUse;\n currentSpan = 1;\n } else {\n currentSpan++;\n }\n }\n });\n\n if (currentMonth !== -1) {\n months.push({ name: monthNames[currentMonth], span: currentSpan, start: weeks.length - currentSpan });\n }\n\n return months;\n}\n\nexport { getMonthsForHeader };\n","// Day and month names derived from a locale, so the component ships no translations.\n//\n// timeZone is pinned to UTC throughout: the reference dates below are built with\n// Date.UTC, and formatting them in a local zone would shift them across a day boundary\n// in negative offsets and hand back the wrong name.\n\n// 7 Jan 2024 is a Sunday, so this week runs Sunday -> Saturday.\nconst REFERENCE_WEEK_START = Date.UTC(2024, 0, 7);\nconst DAYS_IN_WEEK = 7;\n\nfunction getDayNames(locale: string, weekStartsOn: number = 0): string[] {\n const format = new Intl.DateTimeFormat(locale, {\n weekday: 'short',\n timeZone: 'UTC',\n });\n\n return Array.from({ length: DAYS_IN_WEEK }, (_, index) => {\n const dayOfWeek = (weekStartsOn + index) % DAYS_IN_WEEK;\n return format.format(new Date(REFERENCE_WEEK_START + dayOfWeek * 86400000));\n });\n}\n\nfunction getMonthNames(locale: string): string[] {\n const format = new Intl.DateTimeFormat(locale, {\n month: 'short',\n timeZone: 'UTC',\n });\n\n // The 15th avoids any month-length edge case.\n return Array.from({ length: 12 }, (_, month) =>\n format.format(new Date(Date.UTC(2024, month, 15))),\n );\n}\n\nexport { getDayNames, getMonthNames };\n","import { defaultLabels, type HeatmapLabels } from '@/shared/models';\n\nfunction Legend({ labels }: { labels?: HeatmapLabels }): React.ReactNode {\n const text = { ...defaultLabels, ...labels };\n\n return (\n <div className=\"contribution-heatmap__legend\">\n <span className=\"contribution-heatmap__legend-text\">{text.less}</span>\n {[0, 1, 2, 3, 4].map((level) => (\n <div\n key={level}\n className={`contribution-heatmap__legend-item contribution-heatmap__legend-item--level-${level}`}\n title={text.level(level)}\n ></div>\n ))}\n <span className=\"contribution-heatmap__legend-text\">{text.more}</span>\n </div>\n );\n}\n\nexport { Legend };\n","import './index.scss';\nimport type {\n ContributionData,\n HeatmapLabels,\n Period,\n Week as WeekType,\n} from '../shared/models';\nimport { getMonthsForHeader } from './getMonthsForHeader';\nimport { formatTooltip, isInRange } from '@/shared/formatTooltip';\nimport { DEFAULT_LOCALE, parseDateString } from '@/shared/models';\nimport { getDayNames, getMonthNames } from '@/shared/intl';\nimport { Legend } from '@/shared/Legend';\n\n\ninterface DayCellProps {\n contribution: ContributionData;\n period: Period;\n locale: string;\n labels?: HeatmapLabels;\n // Position in the grid, used to stagger the load animation.\n delayIndex: number;\n}\n\n// groupByWeeks pads every week out to Sun-Sat, so the first and last week carry days\n// that fall outside the period. They keep their slot - the columns depend on seven\n// cells per row - but render no square and stay out of the a11y tree.\nfunction DayCell({ contribution, period, locale, labels, delayIndex }: DayCellProps) {\n const inRange = isInRange(contribution.date, period);\n const tooltip = formatTooltip(contribution, period, { locale, labels });\n\n return (\n <td\n className={[\n 'contribution-heatmap__day',\n `contribution-heatmap__day--level-${contribution.level}`,\n inRange ? '' : 'contribution-heatmap__day--outside',\n ]\n .filter(Boolean)\n .join(' ')}\n title={inRange ? tooltip : undefined}\n aria-label={inRange ? tooltip : undefined}\n role={inRange ? 'button' : undefined}\n tabIndex={inRange ? 0 : undefined}\n data-count={contribution.count}\n data-date={contribution.date}\n style={{ '--animation-delay': `${delayIndex * 0.005}s` } as React.CSSProperties}\n ></td>\n );\n}\n\ninterface ContributionHeatmapProps {\n data: { contribution: ContributionData[]; period: Period; weeks: WeekType[] };\n className?: string;\n isReverse?: boolean;\n // Drives day names, month names and the tooltip date through Intl.\n locale?: string;\n // The handful of strings Intl cannot derive.\n labels?: HeatmapLabels;\n}\n\nfunction ContributionHeatmap({\n data: { period, weeks },\n className = '',\n isReverse = false,\n locale = DEFAULT_LOCALE,\n labels,\n}: ContributionHeatmapProps) {\n // Read the week start off the data rather than taking a prop: groupByWeeks already\n // decided it, and a prop could disagree with the weeks it was handed.\n const weekStartsOn = weeks.length ? parseDateString(weeks[0][0].date).getDay() : 0;\n const dayNames = getDayNames(locale, weekStartsOn);\n const months = getMonthsForHeader({ weeks, period, monthNames: getMonthNames(locale) });\n\n return (\n <div\n className={`contribution-heatmap${isReverse ? ' contribution-heatmap--reverse' : ''} ${className}`}\n >\n {/* Only the grid scrolls; the padding and the legend stay put. */}\n <div className=\"contribution-heatmap__scroll\">\n <table className=\"contribution-heatmap__table\">\n {isReverse ? (\n <>\n <thead>\n <tr>\n <th className=\"contribution-heatmap__month-header\"></th>\n {dayNames.map((dayName, dayIndex) => (\n <th key={dayIndex} className=\"contribution-heatmap__day-header\">\n {dayName}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {weeks.map((week, weekIndex) => {\n // Find month info for this week\n const month = months.find(m => weekIndex === m.start);\n const showMonthLabel = !!month;\n return (\n <tr key={weekIndex}>\n {showMonthLabel && (\n <td\n className=\"contribution-heatmap__month-label\"\n rowSpan={month.span}\n >\n {month.name}\n </td>\n )}\n {week.map((contribution, dayIndex) => (\n <DayCell\n key={dayIndex}\n contribution={contribution}\n period={period}\n locale={locale}\n labels={labels}\n delayIndex={weekIndex * 7 + dayIndex}\n />\n ))}\n </tr>\n );\n })}\n </tbody>\n </>\n ) : (\n <>\n <thead>\n <tr>\n <th className=\"contribution-heatmap__day-header\"></th>\n {months.map((month, index) => (\n <th\n key={index}\n className={`contribution-heatmap__month-header${\n index === months.length - 1\n ? ' contribution-heatmap__month-header--last'\n : ''\n }`}\n colSpan={month.span}\n >\n <span className=\"contribution-heatmap__month-header-text\">\n {month.name}\n </span>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {dayNames.map((dayName, dayIndex) => (\n <tr key={dayIndex}>\n <td className=\"contribution-heatmap__day-label\">\n {dayName}\n </td>\n {weeks.map((week, weekIndex) => {\n const contribution = week[dayIndex];\n return (\n <DayCell\n key={weekIndex}\n contribution={contribution}\n period={period}\n locale={locale}\n labels={labels}\n delayIndex={weekIndex * 7 + dayIndex}\n />\n );\n })}\n </tr>\n ))}\n </tbody>\n </>\n )}\n </table>\n </div>\n <Legend labels={labels} />\n </div>\n );\n}\n\nexport { ContributionHeatmap };\n","import {\n createDateString,\n parseDateString,\n type ContributionData,\n type Week,\n} from '@/shared/models';\n\nconst createEmpyContribution = (date: string): ContributionData => ({\n date,\n count: 0,\n level: 0,\n});\n\nconst createContributionMap = (\n contributions: ContributionData[],\n): Map<string, ContributionData> => {\n const contributionMap = new Map<string, ContributionData>();\n contributions.forEach((contribution) => {\n contributionMap.set(contribution.date, contribution);\n });\n return contributionMap;\n};\n\n// Days from weekStartsOn back to the given date's weekday, e.g. with a Monday start\n// (1) a Sunday (0) is six days into the week, not minus one.\nconst daysIntoWeek = (date: Date, weekStartsOn: number): number =>\n (date.getDay() - weekStartsOn + 7) % 7;\n\nconst getFirstDayOfWeek = (firstDate: Date, weekStartsOn: number): Date => {\n const startOfWeek = new Date(firstDate);\n startOfWeek.setDate(firstDate.getDate() - daysIntoWeek(firstDate, weekStartsOn));\n return startOfWeek;\n};\n\nconst getLastDayOfWeek = (lastDate: Date, weekStartsOn: number): Date => {\n const endOfWeek = new Date(lastDate);\n endOfWeek.setDate(lastDate.getDate() + (6 - daysIntoWeek(lastDate, weekStartsOn)));\n return endOfWeek;\n};\n\nconst createWeek = (\n startDate: Date,\n contributionMap: Map<string, ContributionData>,\n): Week => {\n const week: ContributionData[] = [];\n const currentDate = new Date(startDate);\n for (let day = 0; day < 7; day++) {\n const dateString = createDateString(currentDate);\n const contribution = contributionMap.get(dateString);\n\n if (contribution) {\n week.push(contribution);\n } else {\n week.push(createEmpyContribution(dateString));\n }\n currentDate.setDate(currentDate.getDate() + 1);\n }\n\n return week as Week;\n};\n\ninterface GroupByWeeksOptions {\n // 0 = Sunday (the default, and the US/GitHub convention), 1 = Monday, and so on.\n weekStartsOn?: number;\n}\n\nconst groupByWeeks = (\n contributions: ContributionData[],\n { weekStartsOn = 0 }: GroupByWeeksOptions = {},\n): Week[] => {\n const weeks: Week[] = [];\n const contributionMap = createContributionMap(contributions);\n\n const firstDay = getFirstDayOfWeek(parseDateString(contributions[0].date), weekStartsOn);\n const lastDate = getLastDayOfWeek(\n parseDateString(contributions[contributions.length - 1].date),\n weekStartsOn,\n );\n\n const currentDate = new Date(firstDay);\n while (currentDate <= lastDate) {\n weeks.push(createWeek(currentDate, contributionMap));\n currentDate.setDate(currentDate.getDate() + 7);\n }\n\n return weeks;\n};\n\nexport { groupByWeeks };\nexport type { GroupByWeeksOptions };\n","import type { ContributionData } from '@/shared/models';\n\nconst isSummerMonth = (month: number) => {\n return month >= 5 && month <= 7;\n};\n\nexport function createRealisticMockedCount(\n date: Date,\n): ContributionData['count'] {\n // Create realistic patterns\n const dayOfWeek = date.getDay();\n const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;\n const month = date.getMonth();\n\n let baseCount = isWeekend ? 2 : 8;\n\n // Lower activity in summer months (June, July, August)\n if (isSummerMonth(month)) {\n baseCount = Math.max(0, baseCount - 3);\n }\n\n // Add some randomness\n const randomFactor = Math.random();\n let count = 0;\n\n if (randomFactor > 0.3) {\n count = Math.floor(Math.random() * baseCount);\n }\n\n // Occasional high activity days\n if (randomFactor > 0.95) {\n count += Math.floor(Math.random() * 10);\n }\n\n return count;\n}\n","import { createDateString, type ContributionData, type Period } from '@/shared/models';\nimport { createRealisticMockedCount } from './createRealisticMockedCount';\n\nconst getRandomCount = () => Math.floor(Math.random() * 15);\n\nconst getLevel = (count: number): ContributionData['level'] => {\n let level: 0 | 1 | 2 | 3 | 4 = 0;\n if (count === 0) level = 0;\n else if (count <= 2) level = 1;\n else if (count <= 5) level = 2;\n else if (count <= 8) level = 3;\n else level = 4;\n\n return level;\n};\n\nfunction getRange(period: Period | 'lastYear') {\n if (period !== 'lastYear') {\n return period;\n }\n const end = new Date(); // Today\n const start = new Date();\n start.setFullYear(start.getFullYear() - 1); // One year ago\n return { start, end };\n}\n\nfunction generateMockData({\n period,\n isRealistic,\n}: {\n period: Period | 'lastYear';\n isRealistic: boolean;\n}): ContributionData[] {\n const data: ContributionData[] = [];\n const { start, end } = getRange(period);\n\n for (\n let date = new Date(start);\n date <= end;\n date.setDate(date.getDate() + 1)\n ) {\n const count = isRealistic\n ? createRealisticMockedCount(date)\n : getRandomCount();\n data.push({\n date: createDateString(date),\n count,\n level: getLevel(count),\n });\n }\n\n return data;\n}\n\nexport { generateMockData };\n"]}
package/package.json CHANGED
@@ -1,6 +1,17 @@
1
1
  {
2
2
  "name": "@pearpages/heatmap",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
+ "description": "GitHub-style contribution heatmap as a React component.",
5
+ "keywords": [
6
+ "react",
7
+ "heatmap",
8
+ "contribution-graph",
9
+ "calendar-heatmap",
10
+ "github",
11
+ "react-component",
12
+ "activity"
13
+ ],
14
+ "author": "pearpages",
4
15
  "license": "MIT",
5
16
  "repository": {
6
17
  "type": "git",
@@ -9,7 +20,7 @@
9
20
  "bugs": {
10
21
  "url": "https://github.com/pearpages/heatmap/issues"
11
22
  },
12
- "homepage": "https://github.com/pearpages/heatmap#readme",
23
+ "homepage": "https://heatmap.pearpages.com",
13
24
  "type": "module",
14
25
  "workspaces": [
15
26
  "demo"