@pearpages/heatmap 0.3.0 → 0.3.1

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 |
@@ -261,6 +286,21 @@ weeks Monday to Sunday, so a translated heatmap that still starts on Sunday read
261
286
  wrong. It defaults to `0` (Sunday, the GitHub convention), and the component picks the
262
287
  order of its day labels up from the data.
263
288
 
289
+ ## What's new in 0.3.1
290
+
291
+ **Dates are read as local calendar days.** In any time zone other than UTC, 0.3.0 could
292
+ shift the first or last day of a period by one: in Europe the period started a day early
293
+ and the grid grew an extra padding week, and west of Greenwich the days read back a day
294
+ early and the week start flipped to Monday. Every `YYYY-MM-DD` string and every `Period`
295
+ boundary is now a local calendar day. `createDateString` now uses local components, and
296
+ the new `parseDateString` reads a string back as local midnight. See
297
+ [Dates are local calendar days](#dates-are-local-calendar-days).
298
+
299
+ **The package page links the demo,** the README leads with a picture, and a link to the
300
+ demo shows a proper card instead of a bare URL.
301
+
302
+ Nothing to migrate. If you worked around the shifted days yourself, remove the workaround.
303
+
264
304
  ## Local development
265
305
 
266
306
  ```bash
@@ -270,6 +310,7 @@ npm run build # build dist/
270
310
  npm test -- --run # run the test suite once
271
311
  npm run lint
272
312
  npm run check:package # publint + arethetypeswrong
313
+ npm run og -w demo # regenerate the link-preview card, demo/public/og.png (build first)
273
314
  ```
274
315
 
275
316
  `demo/` is a single Vite app that serves as both the development sandbox and the site
package/dist/index.css CHANGED
@@ -307,13 +307,15 @@
307
307
  transform: translateY(0);
308
308
  }
309
309
  }
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;
310
+ @media (prefers-reduced-motion: no-preference) {
311
+ .contribution-heatmap {
312
+ animation: fadeInUp 0.6s ease-out;
313
+ }
314
+ .contribution-heatmap__day::before {
315
+ animation: fadeInUp 0.3s ease-out;
316
+ animation-delay: var(--animation-delay, 0s);
317
+ animation-fill-mode: both;
318
+ }
317
319
  }
318
320
  .contribution-heatmap--ocean {
319
321
  --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 // 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// 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\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":"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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\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.3.1",
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"