├── .github ├── dependabot.yml └── workflows │ ├── playwright.yml │ ├── test.yml │ └── updater.yaml ├── .gitignore ├── .husky └── pre-push ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── components │ └── Container.tsx ├── main.ts ├── manager.ts ├── preview.tsx └── storybook.css ├── LICENSE ├── README.md ├── babel.config.json ├── declarations.d.ts ├── docker-compose.yml ├── docs ├── .nojekyll ├── assets │ ├── ActivityCalendar-Ctsp33F2.css │ ├── ActivityCalendar.stories-DojVutjG.js │ ├── ActivityCalendar.upgrading-CIyRu1Kq.js │ ├── Color-64QXVMR3-CwMUkzOI.js │ ├── DocsRenderer-HT7GNKAR-BivulwmL.js │ ├── Source-BdXYmcyp.js │ ├── formatter-OMEEQ6HG-qqV_F5rT.js │ ├── iframe-PI4EaaR3.js │ ├── iframe-pNs3x2CV.css │ ├── index-BFuEghcR.js │ ├── preload-helper-PPVm8Dsz.js │ ├── react-18-DANgXcre.js │ └── syntaxhighlighter-CAVLW7PM-B_HWZ2GY.js ├── favicon-wrapper.svg ├── favicon.svg ├── iframe.html ├── index.html ├── index.json ├── nunito-sans-bold-italic.woff2 ├── nunito-sans-bold.woff2 ├── nunito-sans-italic.woff2 ├── nunito-sans-regular.woff2 ├── project.json ├── sb-addons │ ├── docs-1 │ │ └── manager-bundle.js │ ├── links-2 │ │ └── manager-bundle.js │ ├── storybook-4 │ │ └── manager-bundle.js │ ├── storybook-core-server-presets-0 │ │ └── common-manager-bundle.js │ └── vueless-storybook-dark-mode-3 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt ├── sb-common-assets │ ├── favicon-wrapper.svg │ ├── favicon.svg │ ├── nunito-sans-bold-italic.woff2 │ ├── nunito-sans-bold.woff2 │ ├── nunito-sans-italic.woff2 │ └── nunito-sans-regular.woff2 ├── sb-manager │ ├── globals-runtime.js │ ├── globals.js │ └── runtime.js └── vite-inject-mocker-entry.js ├── eslint.config.mjs ├── examples ├── customization.tsx ├── event-handlers.tsx ├── labels-shape.tsx ├── labels.tsx ├── ref.tsx ├── themes-explicit.tsx ├── themes.tsx ├── tooltips-config.tsx └── tooltips.tsx ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── rollup.config.mjs ├── screenshot.png ├── src ├── components │ ├── ActivityCalendar.stories.tsx │ ├── ActivityCalendar.tsx │ └── Tooltip.tsx ├── constants.ts ├── docs │ ├── ActivityCalendar.upgrading.mdx │ └── Source.tsx ├── hooks │ ├── useColorScheme.ts │ ├── useLoadingAnimation.ts │ └── usePrefersReducedMotion.ts ├── index.tsx ├── lib │ ├── calendar.test.ts │ ├── calendar.ts │ ├── label.test.ts │ ├── label.ts │ ├── theme.test.ts │ └── theme.ts ├── styles │ ├── styles.ts │ └── tooltips.css └── types.ts ├── tests ├── stories.spec.ts └── stories.spec.ts-snapshots │ ├── activity-levels-chromium-linux.png │ ├── activity-levels-firefox-linux.png │ ├── activity-levels-webkit-linux.png │ ├── color-themes-chromium-linux.png │ ├── color-themes-firefox-linux.png │ ├── color-themes-webkit-linux.png │ ├── container-ref-chromium-linux.png │ ├── container-ref-firefox-linux.png │ ├── container-ref-webkit-linux.png │ ├── customization-chromium-linux.png │ ├── customization-firefox-linux.png │ ├── customization-webkit-linux.png │ ├── date-ranges-chromium-linux.png │ ├── date-ranges-firefox-linux.png │ ├── date-ranges-webkit-linux.png │ ├── default-chromium-linux.png │ ├── default-firefox-linux.png │ ├── default-webkit-linux.png │ ├── event-handlers-chromium-linux.png │ ├── event-handlers-firefox-linux.png │ ├── event-handlers-webkit-linux.png │ ├── explicit-themes-chromium-linux.png │ ├── explicit-themes-firefox-linux.png │ ├── explicit-themes-webkit-linux.png │ ├── loading-chromium-linux.png │ ├── loading-firefox-linux.png │ ├── loading-webkit-linux.png │ ├── localized-labels-chromium-linux.png │ ├── localized-labels-firefox-linux.png │ ├── localized-labels-webkit-linux.png │ ├── monday-as-week-start-chromium-linux.png │ ├── monday-as-week-start-firefox-linux.png │ ├── monday-as-week-start-webkit-linux.png │ ├── narrow-screens-chromium-linux.png │ ├── narrow-screens-firefox-linux.png │ ├── narrow-screens-webkit-linux.png │ ├── tooltips-chromium-linux.png │ ├── tooltips-firefox-linux.png │ ├── tooltips-webkit-linux.png │ ├── weekday-labels-chromium-linux.png │ ├── weekday-labels-firefox-linux.png │ ├── weekday-labels-webkit-linux.png │ ├── without-labels-chromium-linux.png │ ├── without-labels-firefox-linux.png │ └── without-labels-webkit-linux.png ├── tsconfig.json └── vite.config.mts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | groups: 9 | dependencies: 10 | patterns: 11 | - '*' 12 | update-types: 13 | - 'minor' 14 | - 'patch' 15 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: # manually 10 | jobs: 11 | test: 12 | timeout-minutes: 60 13 | runs-on: ubuntu-latest 14 | container: 15 | image: mcr.microsoft.com/playwright:v1.56.1-noble 16 | options: --user 1001 17 | steps: 18 | - uses: actions/checkout@v6.0.0 19 | - uses: actions/setup-node@v6.0.0 20 | with: 21 | node-version: lts/* 22 | 23 | - uses: pnpm/action-setup@v4.2.0 24 | name: Install pnpm 25 | with: 26 | version: latest 27 | run_install: false 28 | 29 | - name: Install dependencies 30 | run: pnpm install -r --ignore-scripts 31 | 32 | - name: Run Playwright tests 33 | run: npx playwright test 34 | 35 | - uses: actions/upload-artifact@v5.0.0 36 | if: ${{ !cancelled() }} 37 | with: 38 | name: .playwright-report 39 | path: .playwright-report/ 40 | include-hidden-files: true 41 | retention-days: 30 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6.0.0 11 | 12 | - uses: actions/setup-node@v6.0.0 13 | with: 14 | node-version: node 15 | 16 | - uses: pnpm/action-setup@v4.2.0 17 | name: Install pnpm 18 | with: 19 | version: latest 20 | run_install: false 21 | 22 | - name: Get pnpm store directory 23 | shell: bash 24 | run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 25 | 26 | - uses: actions/cache@v4.3.0 27 | name: Setup pnpm cache 28 | with: 29 | path: ${{ env.STORE_PATH }} 30 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | restore-keys: ${{ runner.os }}-pnpm-store- 32 | 33 | - name: Install dependencies 34 | run: pnpm install -r --ignore-scripts 35 | 36 | - name: Check formatting (Prettier) 37 | run: pnpx prettier -c . 38 | 39 | - name: Run unit tests 40 | run: pnpm test:unit 41 | 42 | - name: Run type checks (TypeScript) 43 | run: pnpm tsc 44 | 45 | - name: Run linter (ESLint) 46 | run: pnpm lint 47 | -------------------------------------------------------------------------------- /.github/workflows/updater.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub actions updater 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * 0' # every Sunday 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6.0.0 12 | with: 13 | token: ${{ secrets.WORKFLOW_SECRET }} # Access token with `workflow` scope 14 | 15 | - name: Run GitHub Actions Version Updater 16 | uses: saadmk11/github-actions-version-updater@v0.9.0 17 | with: 18 | # [Required] Access token with `workflow` scope. 19 | token: ${{ secrets.WORKFLOW_SECRET }} # Access token with `workflow` scope 20 | committer_username: 'Jonathan Gruber' 21 | committer_email: 'gruberjonathan@gmail.com' 22 | commit_message: 'Update GitHub actions' 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | coverage/ 4 | .playwright/ 5 | .playwright-report/ 6 | bundle.html 7 | 8 | .env.* 9 | .log* 10 | 11 | .vscode/ 12 | .idea/ 13 | .directory 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm check 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /build 2 | /docs 3 | /examples 4 | pnpm-lock.yaml 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@ianvs/prettier-plugin-sort-imports"], 3 | "importOrder": ["", "^react$", "", "^[.]"], 4 | "importOrderParserPlugins": ["typescript", "jsx", "importAttributes"], 5 | "importOrderTypeScriptVersion": "5.7.0", 6 | "arrowParens": "avoid", 7 | "printWidth": 100, 8 | "proseWrap": "always", 9 | "singleQuote": true, 10 | "semi": false, 11 | "overrides": [ 12 | { 13 | "files": "*.d.ts", 14 | "options": { 15 | "importOrderParserPlugins": ["[\"typescript\", { \"dts\": true }]"] 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.storybook/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | const Container = ({ children }: { children: ReactNode }) => { 4 | return
{children}
5 | } 6 | 7 | export default Container 8 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | 3 | const config: StorybookConfig = { 4 | framework: '@storybook/react-vite', 5 | stories: ['../src/**/*.stories.@(ts|tsx)', '../src/**/*.mdx'], 6 | addons: ['@storybook/addon-docs', '@storybook/addon-links', '@vueless/storybook-dark-mode'], 7 | features: { 8 | actions: false, 9 | interactions: false, 10 | }, 11 | typescript: { 12 | reactDocgen: 'react-docgen', 13 | }, 14 | } 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga4' 2 | 3 | ReactGA.initialize('G-V30ERZKLWJ', { 4 | gtagOptions: { 5 | anonymizeIp: true, 6 | content_group: 'storybook', 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { DocsContainer, type DocsContainerProps } from '@storybook/addon-docs/blocks' 3 | import type { Preview } from '@storybook/react-vite' 4 | import { DARK_MODE_EVENT_NAME } from '@vueless/storybook-dark-mode' 5 | import { themes, type ThemeVarsPartial } from 'storybook/theming' 6 | import './storybook.css' 7 | 8 | const baseTheme = { 9 | base: 'light', 10 | brandTitle: 'React Activity Calendar', 11 | brandUrl: 'https://github.com/grubersjoe/react-activity-calendar', 12 | } satisfies ThemeVarsPartial 13 | 14 | const themeOverride = { 15 | fontBase: 'ui-sans-serif, sans-serif', 16 | fontCode: 'ui-monospace, monospace', 17 | } satisfies Omit 18 | 19 | // This component is a workaround for the dark mode addon not being available on 20 | // docs pages: https://github.com/hipstersmoothie/storybook-dark-mode/issues/282 21 | const Container = (props: DocsContainerProps) => { 22 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches 23 | const [theme, setTheme] = useState(prefersDark ? themes.dark : themes.light) 24 | 25 | useEffect(() => { 26 | const listener = (isDark: boolean) => { 27 | setTheme(isDark ? themes.dark : themes.light) 28 | } 29 | 30 | props.context.channel.on(DARK_MODE_EVENT_NAME, listener) 31 | 32 | return () => { 33 | props.context.channel.removeListener(DARK_MODE_EVENT_NAME, listener) 34 | } 35 | }, [props.context.channel]) 36 | 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | export const preview: Preview = { 50 | parameters: { 51 | docs: { 52 | toc: true, 53 | codePanel: true, 54 | container: Container, 55 | controls: { 56 | sort: 'alpha', 57 | }, 58 | source: { 59 | language: 'tsx', 60 | }, 61 | }, 62 | darkMode: { 63 | stylePreview: true, 64 | dark: { ...baseTheme, ...themes.dark, ...themeOverride }, 65 | light: { ...baseTheme, ...themes.light, ...themeOverride }, 66 | }, 67 | }, 68 | } 69 | 70 | export default preview 71 | -------------------------------------------------------------------------------- /.storybook/storybook.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: ui-sans-serif, sans-serif; 7 | font-size: 16px; 8 | line-height: 1.5; 9 | } 10 | 11 | h1, 12 | h2, 13 | h3 { 14 | /* Unfortunately, the Storybook documentation pages use very specific selectors. */ 15 | /* So we have to use !important a lot. */ 16 | margin: 1em 0 0.375em !important; 17 | border: none !important; 18 | padding: 0 !important; 19 | } 20 | 21 | h1 { 22 | margin-top: 0; 23 | } 24 | 25 | p { 26 | margin: 0 0 1.25em !important; 27 | } 28 | 29 | p, 30 | ul, 31 | ol { 32 | margin-top: 0 !important; 33 | } 34 | 35 | p, 36 | ul, 37 | ol, 38 | pre { 39 | max-width: 720px !important; 40 | } 41 | 42 | p { 43 | font-size: inherit !important; 44 | } 45 | 46 | code, 47 | pre { 48 | font-family: ui-monospace, monospace !important; 49 | } 50 | 51 | code { 52 | font-size: 13px !important; 53 | } 54 | 55 | pre { 56 | &, 57 | span { 58 | font-family: ui-monospace, monospace !important; 59 | font-size: 14px !important; 60 | } 61 | } 62 | 63 | a:not(:is(aside a)) { 64 | color: inherit !important; 65 | text-decoration: underline !important; 66 | text-underline-offset: 3px !important; 67 | text-decoration-color: #bbb !important; 68 | 69 | &:hover { 70 | text-decoration-color: #888 !important; 71 | } 72 | } 73 | 74 | #storybook-root { 75 | overflow: hidden; 76 | } 77 | 78 | .dark { 79 | &.sb-show-main { 80 | color: #fff; 81 | background-color: hsl(0, 0%, 12%); 82 | } 83 | 84 | .docblock-source { 85 | background: hsl(0, 0%, 12%); 86 | } 87 | } 88 | 89 | .sb-show-main.sb-main-centered #storybook-root { 90 | padding: 2.5rem; /* weekday labels */ 91 | } 92 | 93 | .sbdocs { 94 | td pre > code { 95 | white-space: pre !important; 96 | } 97 | } 98 | 99 | .sbdocs-wrapper { 100 | padding: 1rem 3rem !important; 101 | } 102 | 103 | .sbdocs-content li { 104 | font-size: inherit !important; 105 | margin-top: 0.5em !important; 106 | } 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jonathan Gruber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Activity Calendar 2 | 3 | [![CI](https://github.com/grubersjoe/react-activity-calendar/actions/workflows/test.yml/badge.svg)](https://github.com/grubersjoe/react-activity-calendar/actions/workflows/test.yml) 4 | [![npm version](https://badge.fury.io/js/react-activity-calendar.svg)](https://www.npmjs.com/package/react-activity-calendar) 5 | 6 | A React component to display activity data in a calendar (heatmap).
7 | [Documentation (Storybook)](https://grubersjoe.github.io/react-activity-calendar) 8 | 9 | **Version 3** has been 10 | [released](https://github.com/grubersjoe/react-activity-calendar/releases/tag/v3.0) 🎉
See the 11 | [upgrade guide](https://grubersjoe.github.io/react-activity-calendar/?path=/docs/react-activity-calendar-upgrading-to-v3--docs). 12 | 13 | ![Screenshot](screenshot.png) 14 | 15 | 16 | Buy Me A Coffee 17 | 18 | 19 | ## Installation 20 | 21 | ```shell 22 | npm install react-activity-calendar 23 | ``` 24 | 25 | ## Features 26 | 27 | - any number of activity levels 📈 28 | - color themes 🌈 29 | - dark & light mode ✨ 30 | - tooltips 🪧 31 | - event handlers ⁉️ 32 | - localization 🌍 33 | 34 | The component expects activity data in the following structure. Each activity level must be in the 35 | interval from 0 to `maxLevel`, which is 4 by default (see 36 | [documentation](https://grubersjoe.github.io/react-activity-calendar/?path=/story/react-activity-calendar--activity-levels)). 37 | It is up to you how to generate and classify your data. 38 | 39 | ```tsx 40 | import { ActivityCalendar } from 'react-activity-calendar' 41 | 42 | const data = [ 43 | { 44 | date: '2024-06-23', 45 | count: 2, 46 | level: 1, 47 | }, 48 | { 49 | date: '2024-08-02', 50 | count: 16, 51 | level: 4, 52 | }, 53 | { 54 | date: '2024-11-29', 55 | count: 11, 56 | level: 3, 57 | }, 58 | ] 59 | 60 | function Calendar() { 61 | return 62 | } 63 | ``` 64 | 65 | ## FAQ 66 | 67 | ### Why does the calendar not render in environment x? 68 | 69 | If you encounter issues rendering this component in a specific React framework, please refer to the 70 | following repository. It contains working examples for Astro, Next.js, Remix and Vite. Server side 71 | rendering (SSR) is supported. 72 | 73 | [Framework examples](https://github.com/grubersjoe/react-activity-calendar-tests) 74 | 75 | ### Why is Create React App unsupported? 76 | 77 | Create React App (CRA) is considered 78 | [abandoned](https://github.com/facebook/create-react-app/discussions/11086), and you probably should 79 | not use it anymore (more 80 | [background](https://github.com/facebook/create-react-app/issues/11180#issuecomment-874748552)). 81 | Using this component inside CRA will lead to errors for reasons described in issue 82 | [#105](https://github.com/grubersjoe/react-activity-calendar/issues/105). This repo is not for CRA 83 | support questions. If you encounter issues, you need to fix those yourself given the maintenance 84 | state of CRA. Personally, I would recommend using [Vite](https://vitejs.dev/) instead of CRA. 85 | 86 | ## Development 87 | 88 | ### Start the Storybook 89 | 90 | ```shell 91 | npm run storybook 92 | ``` 93 | 94 | ### Update the documentation 95 | 96 | ```shell 97 | npm run build:storybook 98 | ``` 99 | 100 | ## Related projects 101 | 102 | - [grubersjoe/react-github-calendar](https://github.com/grubersjoe/react-github-calendar) 103 | - [grubersjoe/github-contributions-api](https://github.com/grubersjoe/github-contributions-api) 104 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | [ 6 | "@babel/preset-react", 7 | { 8 | "runtime": "automatic" 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*?raw' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | playwright: 3 | image: mcr.microsoft.com/playwright:v1.56.1-noble 4 | platform: linux/arm64 5 | working_dir: /app 6 | volumes: 7 | - .:/app 8 | - playwright-node-modules:/app/node_modules 9 | stdin_open: true 10 | tty: true 11 | 12 | volumes: 13 | playwright-node-modules: 14 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/ActivityCalendar-Ctsp33F2.css: -------------------------------------------------------------------------------- 1 | .react-activity-calendar__tooltip{width:max-content;max-width:calc(100vw - 20px);padding:.2em .5em;border-radius:.25em;background-color:#1a1a1a;color:#f0f0f0;font-size:13px}.react-activity-calendar__tooltip .react-activity-calendar__tooltip-arrow{fill:#1a1a1a}.react-activity-calendar__tooltip[data-color-scheme=dark]{background-color:#f0f0f0;color:#0f0f0f}.react-activity-calendar__tooltip[data-color-scheme=dark] .react-activity-calendar__tooltip-arrow{fill:#f0f0f0} 2 | -------------------------------------------------------------------------------- /docs/assets/ActivityCalendar.upgrading-CIyRu1Kq.js: -------------------------------------------------------------------------------- 1 | import{j as e,M as d}from"./iframe-PI4EaaR3.js";import{useMDXComponents as i}from"./index-BFuEghcR.js";import{S as o,r}from"./Source-BdXYmcyp.js";import"./preload-helper-PPVm8Dsz.js";function s(t){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...i(),...t.components};return e.jsxs(e.Fragment,{children:[e.jsx(d,{title:"React Activity Calendar/Upgrading to v3"}),` 2 | `,e.jsx(n.h1,{id:"upgrading-to-v3",children:"Upgrading to v3"}),` 3 | `,e.jsx(n.p,{children:`Version 3 of React Activity Calendar introduces several breaking changes and a introduces a new 4 | approach to tooltips. Follow the guide below to upgrade your project from v2 to v3.`}),` 5 | `,e.jsx(n.h2,{id:"breaking-changes",children:"Breaking changes"}),` 6 | `,e.jsxs(n.ul,{children:[` 7 | `,e.jsxs(n.li,{children:[`React Activity Calendar is now a 8 | `,e.jsx(n.a,{href:"https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c",rel:"nofollow",children:"pure ESM package"}),"."]}),` 9 | `,e.jsxs(n.li,{children:["The default export has been ",e.jsx(n.strong,{children:"removed"}),". Use the named export instead:",` 10 | `,e.jsx(o,{code:"import { ActivityCalendar } from 'react-activity-calendar'"}),` 11 | `]}),` 12 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:"eventHandlers"})," prop has been ",e.jsx(n.strong,{children:"removed"}),". Use the ",e.jsx(n.code,{children:"renderBlock"}),` prop with 13 | `,e.jsx(n.code,{children:"React.cloneElement()"})," to ",e.jsx(r,{kind:"react-activity-calendar",name:"event-handlers",children:`attach 14 | event handlers`}),"."]}),` 15 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:"totalCount"})," prop has been ",e.jsx(n.strong,{children:"removed"}),", overriding the total count is no longer supported."]}),` 16 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:"hideColorLegend"})," prop has been ",e.jsx(n.strong,{children:"renamed"})," to ",e.jsx(n.code,{children:"showColorLegend"}),"."]}),` 17 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:"hideMonthLabels"})," prop has been ",e.jsx(n.strong,{children:"renamed"})," to ",e.jsx(n.code,{children:"showMonthLabels"}),"."]}),` 18 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:"hideTotalCount"})," prop has been ",e.jsx(n.strong,{children:"renamed"})," to ",e.jsx(n.code,{children:"showTotalCount"}),"."]}),` 19 | `,e.jsxs(n.li,{children:["The ",e.jsx(n.code,{children:""})," component has been ",e.jsx(n.strong,{children:"removed"}),".",e.jsx("br",{}),` Render the calendar with empty data in 20 | its loading state instead:`,` 21 | `,e.jsx(o,{code:""}),` 22 | `]}),` 23 | `]}),` 24 | `,e.jsx(n.h2,{id:"tooltips",children:"Tooltips"}),` 25 | `,e.jsxs(n.p,{children:[`Tooltips no longer depend on external libraries and are now integrated directly into this package. 26 | Thanks to code-splitting, tooltips only affect your bundle size when you use them. They are 27 | implemented using the `,e.jsx(n.a,{href:"https://floating-ui.com/",rel:"nofollow",children:"Floating UI"}),` library as a “headless” component, 28 | meaning they come without predefined styles. This gives you full control over the appearance:`]}),` 29 | `,e.jsxs(n.ul,{children:[` 30 | `,e.jsxs(n.li,{children:["Import the default styles provided by this package, ",e.jsx(n.strong,{children:"or"})]}),` 31 | `,e.jsx(n.li,{children:"Add your own custom CSS."}),` 32 | `]}),` 33 | `,e.jsxs(n.p,{children:["See the ",e.jsx(r,{kind:"react-activity-calendar",name:"tooltips",children:"tooltips"}),` page for details 34 | and examples.`]})]})}function p(t={}){const{wrapper:n}={...i(),...t.components};return n?e.jsx(n,{...t,children:e.jsx(s,{...t})}):s(t)}export{p as default}; 35 | -------------------------------------------------------------------------------- /docs/assets/DocsRenderer-HT7GNKAR-BivulwmL.js: -------------------------------------------------------------------------------- 1 | const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index-BFuEghcR.js","./iframe-PI4EaaR3.js","./preload-helper-PPVm8Dsz.js","./iframe-pNs3x2CV.css"])))=>i.map(i=>d[i]); 2 | import{_ as E}from"./preload-helper-PPVm8Dsz.js";import{R as t,H as h,A as l,C as _,D,r as v,_ as n}from"./iframe-PI4EaaR3.js";import{renderElement as x,unmountElement as y}from"./react-18-DANgXcre.js";var R={code:_,a:l,...h},s=class extends v.Component{constructor(){super(...arguments),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(r){const{showException:e}=this.props;e(r)}render(){const{hasError:r}=this.state,{children:e}=this.props;return r?null:t.createElement(t.Fragment,null,e)}};n(s,"ErrorBoundary");var f=s,a=class{constructor(){this.render=async(r,e,m)=>{const d={...R,...e?.components},u=D;return new Promise((i,p)=>{E(async()=>{const{MDXProvider:o}=await import("./index-BFuEghcR.js");return{MDXProvider:o}},__vite__mapDeps([0,1,2,3]),import.meta.url).then(({MDXProvider:o})=>x(t.createElement(f,{showException:p,key:Math.random()},t.createElement(o,{components:d},t.createElement(u,{context:r,docsParameter:e}))),m)).then(()=>i())})},this.unmount=r=>{y(r)}}};n(a,"DocsRenderer");var A=a;export{A as DocsRenderer,R as defaultComponents}; 3 | -------------------------------------------------------------------------------- /docs/assets/formatter-OMEEQ6HG-qqV_F5rT.js: -------------------------------------------------------------------------------- 1 | import{d as i,e as t,f as a}from"./iframe-PI4EaaR3.js";import"./preload-helper-PPVm8Dsz.js";var m=i(a(),1),d=(0,m.default)(2)(async(e,r)=>e===!1?r:t(r));export{d as formatter}; 2 | -------------------------------------------------------------------------------- /docs/assets/iframe-pNs3x2CV.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box}body{font-family:ui-sans-serif,sans-serif;font-size:16px;line-height:1.5}h1,h2,h3{margin:1em 0 .375em!important;border:none!important;padding:0!important}h1{margin-top:0}p{margin:0 0 1.25em!important}p,ul,ol{margin-top:0!important}p,ul,ol,pre{max-width:720px!important}p{font-size:inherit!important}code,pre{font-family:ui-monospace,monospace!important}code{font-size:13px!important}pre,pre span{font-family:ui-monospace,monospace!important;font-size:14px!important}a:not(:is(aside a)){color:inherit!important;text-decoration:underline!important;text-underline-offset:3px!important;text-decoration-color:#bbb!important}a:not(:is(aside a)):hover{text-decoration-color:#888!important}#storybook-root{overflow:hidden}.dark.sb-show-main{color:#fff;background-color:#1f1f1f}.dark .docblock-source{background:#1f1f1f}.sb-show-main.sb-main-centered #storybook-root{padding:2.5rem}.sbdocs td pre>code{white-space:pre!important}.sbdocs-wrapper{padding:1rem 3rem!important}.sbdocs-content li{font-size:inherit!important;margin-top:.5em!important} 2 | -------------------------------------------------------------------------------- /docs/assets/index-BFuEghcR.js: -------------------------------------------------------------------------------- 1 | import{R as e}from"./iframe-PI4EaaR3.js";import"./preload-helper-PPVm8Dsz.js";const o={},c=e.createContext(o);function u(n){const t=e.useContext(c);return e.useMemo(function(){return typeof n=="function"?n(t):{...t,...n}},[t,n])}function r(n){let t;return n.disableParentContext?t=typeof n.components=="function"?n.components(o):n.components||o:t=u(n.components),e.createElement(c.Provider,{value:t},n.children)}export{r as MDXProvider,u as useMDXComponents}; 2 | -------------------------------------------------------------------------------- /docs/assets/preload-helper-PPVm8Dsz.js: -------------------------------------------------------------------------------- 1 | const p="modulepreload",y=function(u,i){return new URL(u,i).href},v={},w=function(i,a,f){let d=Promise.resolve();if(a&&a.length>0){let E=function(e){return Promise.all(e.map(o=>Promise.resolve(o).then(s=>({status:"fulfilled",value:s}),s=>({status:"rejected",reason:s}))))};const r=document.getElementsByTagName("link"),t=document.querySelector("meta[property=csp-nonce]"),m=t?.nonce||t?.getAttribute("nonce");d=E(a.map(e=>{if(e=y(e,f),e in v)return;v[e]=!0;const o=e.endsWith(".css"),s=o?'[rel="stylesheet"]':"";if(f)for(let c=r.length-1;c>=0;c--){const l=r[c];if(l.href===e&&(!o||l.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${e}"]${s}`))return;const n=document.createElement("link");if(n.rel=o?"stylesheet":p,o||(n.as="script"),n.crossOrigin="",n.href=e,m&&n.setAttribute("nonce",m),document.head.appendChild(n),o)return new Promise((c,l)=>{n.addEventListener("load",c),n.addEventListener("error",()=>l(new Error(`Unable to preload CSS for ${e}`)))})}))}function h(r){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=r,window.dispatchEvent(t),!t.defaultPrevented)throw r}return d.then(r=>{for(const t of r||[])t.status==="rejected"&&h(t.reason);return i().catch(h)})};export{w as _}; 2 | -------------------------------------------------------------------------------- /docs/favicon-wrapper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 34 | 36 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Storybook 7 | 8 | 9 | 42 | 43 | 60 | 61 | 62 | 517 | 518 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 |
539 |
540 |
541 | 542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 572 | 575 | 576 | 577 | 578 | 579 | 585 | 588 | 589 | 590 | 591 | 592 | 598 | 601 | 602 | 603 | 604 | 605 |
606 | 607 |
608 |
609 |

No Preview

610 |

Sorry, but you either have no stories or none are selected somehow.

611 |
    612 |
  • Please check the Storybook config.
  • 613 |
  • Try reloading the page.
  • 614 |
615 |

616 | If the problem persists, check the browser console, or the terminal you've run Storybook from. 617 |

618 |
619 |
620 | 621 |
622 |
623 |

624 |

625 | The component failed to render properly, likely due to a configuration issue in Storybook. 626 | Here are some common causes and how you can address them: 627 |

628 |
    629 |
  1. 630 | Missing Context/Providers: You can use decorators to supply specific 631 | contexts or providers, which are sometimes necessary for components to render correctly. For 632 | detailed instructions on using decorators, please visit the 633 | Decorators documentation. 636 |
  2. 637 |
  3. 638 | Misconfigured Webpack or Vite: Verify that Storybook picks up all necessary 639 | settings for loaders, plugins, and other relevant parameters. You can find step-by-step 640 | guides for configuring 641 | Webpack or 642 | Vite 643 | with Storybook. 644 |
  4. 645 |
  5. 646 | Missing Environment Variables: Your Storybook may require specific 647 | environment variables to function as intended. You can set up custom environment variables 648 | as outlined in the 649 | Environment Variables documentation. 652 |
  6. 653 |
654 |
655 |
656 |
657 | 658 |
659 |
660 | 686 | 687 | 688 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | storybook - Storybook 7 | 8 | 9 | 10 | 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 | 129 | 130 | 131 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /docs/index.json: -------------------------------------------------------------------------------- 1 | {"v":5,"entries":{"react-activity-calendar--docs":{"id":"react-activity-calendar--docs","title":"React Activity Calendar","name":"Docs","importPath":"./src/components/ActivityCalendar.stories.tsx","type":"docs","tags":["dev","test","autodocs"],"storiesImports":[]},"react-activity-calendar--default":{"type":"story","subtype":"story","id":"react-activity-calendar--default","name":"Default","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"Default"},"react-activity-calendar--loading":{"type":"story","subtype":"story","id":"react-activity-calendar--loading","name":"Loading","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"Loading"},"react-activity-calendar--activity-levels":{"type":"story","subtype":"story","id":"react-activity-calendar--activity-levels","name":"Activity Levels","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"ActivityLevels"},"react-activity-calendar--date-ranges":{"type":"story","subtype":"story","id":"react-activity-calendar--date-ranges","name":"Date Ranges","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"DateRanges"},"react-activity-calendar--color-themes":{"type":"story","subtype":"story","id":"react-activity-calendar--color-themes","name":"Color Themes","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"ColorThemes"},"react-activity-calendar--explicit-themes":{"type":"story","subtype":"story","id":"react-activity-calendar--explicit-themes","name":"Explicit Themes","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"ExplicitThemes"},"react-activity-calendar--customization":{"type":"story","subtype":"story","id":"react-activity-calendar--customization","name":"Customization","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"Customization"},"react-activity-calendar--event-handlers":{"type":"story","subtype":"story","id":"react-activity-calendar--event-handlers","name":"Event Handlers","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"EventHandlers"},"react-activity-calendar--tooltips":{"type":"story","subtype":"story","id":"react-activity-calendar--tooltips","name":"Tooltips","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"Tooltips"},"react-activity-calendar--without-labels":{"type":"story","subtype":"story","id":"react-activity-calendar--without-labels","name":"Without Labels","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"WithoutLabels"},"react-activity-calendar--weekday-labels":{"type":"story","subtype":"story","id":"react-activity-calendar--weekday-labels","name":"Weekday Labels","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"WeekdayLabels"},"react-activity-calendar--localized-labels":{"type":"story","subtype":"story","id":"react-activity-calendar--localized-labels","name":"Localized Labels","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"LocalizedLabels"},"react-activity-calendar--monday-as-week-start":{"type":"story","subtype":"story","id":"react-activity-calendar--monday-as-week-start","name":"Monday As Week Start","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"MondayAsWeekStart"},"react-activity-calendar--narrow-screens":{"type":"story","subtype":"story","id":"react-activity-calendar--narrow-screens","name":"Narrow Screens","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"NarrowScreens"},"react-activity-calendar--container-ref":{"type":"story","subtype":"story","id":"react-activity-calendar--container-ref","name":"Container Ref","title":"React Activity Calendar","importPath":"./src/components/ActivityCalendar.stories.tsx","componentPath":"./src/components/ActivityCalendar.tsx","tags":["dev","test","autodocs"],"exportName":"ContainerRef"},"react-activity-calendar-upgrading-to-v3--docs":{"id":"react-activity-calendar-upgrading-to-v3--docs","title":"React Activity Calendar/Upgrading to v3","name":"Docs","importPath":"./src/docs/ActivityCalendar.upgrading.mdx","storiesImports":[],"type":"docs","tags":["dev","test","unattached-mdx"]}}} -------------------------------------------------------------------------------- /docs/nunito-sans-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/nunito-sans-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/nunito-sans-bold.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/nunito-sans-italic.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/nunito-sans-regular.woff2 -------------------------------------------------------------------------------- /docs/project.json: -------------------------------------------------------------------------------- 1 | {"generatedAt":1763219424437,"userSince":1748507106026,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@jest/globals":"30.2.0","@jest/types":"30.2.0","@playwright/test":"1.56.1","jest":"30.2.0","jest-environment-jsdom":"30.2.0"},"hasRouterPackage":false,"packageManager":{"type":"pnpm","agent":"pnpm","nodeLinker":"undefined"},"typescriptOptions":{"reactDocgen":"react-docgen"},"features":{"actions":false,"interactions":false},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":1,"applicationFileCount":0,"storybookVersion":"10.0.7","language":"typescript","storybookPackages":{"@storybook/react-vite":{"version":"10.0.7"},"eslint-plugin-storybook":{"version":"10.0.7"},"storybook":{}},"addons":{"@storybook/addon-docs":{"version":"10.0.7"},"@storybook/addon-links":{"version":"10.0.7"},"@vueless/storybook-dark-mode":{"version":"10.0.2"}}} -------------------------------------------------------------------------------- /docs/sb-addons/docs-1/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var S=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var Be=Object.defineProperty;var o=(r,e)=>Be(r,"name",{value:e,configurable:!0}),yr=(r=>typeof S<"u"?S:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof S<"u"?S:e)[t]}):r)(function(r){if(typeof S<"u")return S.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var d=__REACT__,{Children:wr,Component:Tr,Fragment:Pr,Profiler:Or,PureComponent:Rr,StrictMode:Cr,Suspense:Er,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:Ir,act:Fr,cloneElement:jr,createContext:Hr,createElement:Nr,createFactory:zr,createRef:Ar,forwardRef:Mr,isValidElement:Br,lazy:Dr,memo:Lr,startTransition:qr,unstable_act:$r,useCallback:Ur,useContext:Wr,useDebugValue:Yr,useDeferredValue:Gr,useEffect:X,useId:Kr,useImperativeHandle:Jr,useInsertionEffect:Zr,useLayoutEffect:Qr,useMemo:Xr,useReducer:Vr,useRef:et,useState:V,useSyncExternalStore:rt,useTransition:tt,version:at}=__REACT__;var ut=__STORYBOOK_COMPONENTS__,{A:pt,ActionBar:lt,AddonPanel:ee,Badge:dt,Bar:ft,Blockquote:ct,Button:mt,ClipboardCode:gt,Code:ht,DL:bt,Div:yt,DocumentWrapper:vt,EmptyTabContent:_t,ErrorFormatter:xt,FlexBar:St,Form:kt,H1:wt,H2:Tt,H3:Pt,H4:Ot,H5:Rt,H6:Ct,HR:Et,IconButton:It,Img:Ft,LI:jt,Link:Ht,ListItem:Nt,Loader:zt,Modal:At,OL:Mt,P:Bt,Placeholder:Dt,Pre:Lt,ProgressSpinner:qt,ResetWrapper:$t,ScrollArea:Ut,Separator:Wt,Spaced:Yt,Span:Gt,StorybookIcon:Kt,StorybookLogo:Jt,SyntaxHighlighter:re,TT:Zt,TabBar:Qt,TabButton:Xt,TabWrapper:Vt,Table:ea,Tabs:ra,TabsState:ta,TooltipLinkList:aa,TooltipMessage:na,TooltipNote:oa,UL:sa,WithTooltip:ia,WithTooltipPure:ua,Zoom:pa,codeCommon:la,components:da,createCopyToClipboardFunction:fa,getStoryHref:ca,interleaveSeparators:ma,nameSpaceClassNames:ga,resetComponents:ha,withReset:te}=__STORYBOOK_COMPONENTS__;var xa=__STORYBOOK_THEMING__,{CacheProvider:Sa,ClassNames:ka,Global:wa,ThemeProvider:ae,background:Ta,color:Pa,convert:ne,create:Oa,createCache:Ra,createGlobal:Ca,createReset:Ea,css:Ia,darken:Fa,ensure:ja,ignoreSsrWarning:H,isPropValid:Ha,jsx:Na,keyframes:za,lighten:Aa,styled:_,themes:q,typography:Ma,useTheme:N,withTheme:Ba}=__STORYBOOK_THEMING__;function f(){return f=Object.assign?Object.assign.bind():function(r){for(var e=1;e=0&&n<1?(u=s,p=i):n>=1&&n<2?(u=i,p=s):n>=2&&n<3?(p=s,l=i):n>=3&&n<4?(p=i,l=s):n>=4&&n<5?(u=i,l=s):n>=5&&n<6&&(u=s,l=i);var b=t-s/2,g=u+b,h=p+b,E=l+b;return a(g,h,E)}o(C,"hslToRgb");var oe={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"639",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"};function fe(r){if(typeof r!="string")return r;var e=r.toLowerCase();return oe[e]?"#"+oe[e]:r}o(fe,"nameToHex");var $e=/^#[a-fA-F0-9]{6}$/,Ue=/^#[a-fA-F0-9]{8}$/,We=/^#[a-fA-F0-9]{3}$/,Ye=/^#[a-fA-F0-9]{4}$/,$=/^rgb\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*\)$/i,Ge=/^rgb(?:a)?\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i,Ke=/^hsl\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*\)$/i,Je=/^hsl(?:a)?\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i;function P(r){if(typeof r!="string")throw new c(3);var e=fe(r);if(e.match($e))return{red:parseInt(""+e[1]+e[2],16),green:parseInt(""+e[3]+e[4],16),blue:parseInt(""+e[5]+e[6],16)};if(e.match(Ue)){var t=parseFloat((parseInt(""+e[7]+e[8],16)/255).toFixed(2));return{red:parseInt(""+e[1]+e[2],16),green:parseInt(""+e[3]+e[4],16),blue:parseInt(""+e[5]+e[6],16),alpha:t}}if(e.match(We))return{red:parseInt(""+e[1]+e[1],16),green:parseInt(""+e[2]+e[2],16),blue:parseInt(""+e[3]+e[3],16)};if(e.match(Ye)){var a=parseFloat((parseInt(""+e[4]+e[4],16)/255).toFixed(2));return{red:parseInt(""+e[1]+e[1],16),green:parseInt(""+e[2]+e[2],16),blue:parseInt(""+e[3]+e[3],16),alpha:a}}var n=$.exec(e);if(n)return{red:parseInt(""+n[1],10),green:parseInt(""+n[2],10),blue:parseInt(""+n[3],10)};var s=Ge.exec(e.substring(0,50));if(s)return{red:parseInt(""+s[1],10),green:parseInt(""+s[2],10),blue:parseInt(""+s[3],10),alpha:parseFloat(""+s[4])>1?parseFloat(""+s[4])/100:parseFloat(""+s[4])};var i=Ke.exec(e);if(i){var u=parseInt(""+i[1],10),p=parseInt(""+i[2],10)/100,l=parseInt(""+i[3],10)/100,b="rgb("+C(u,p,l)+")",g=$.exec(b);if(!g)throw new c(4,e,b);return{red:parseInt(""+g[1],10),green:parseInt(""+g[2],10),blue:parseInt(""+g[3],10)}}var h=Je.exec(e.substring(0,50));if(h){var E=parseInt(""+h[1],10),Ae=parseInt(""+h[2],10)/100,Me=parseInt(""+h[3],10)/100,Q="rgb("+C(E,Ae,Me)+")",j=$.exec(Q);if(!j)throw new c(4,e,Q);return{red:parseInt(""+j[1],10),green:parseInt(""+j[2],10),blue:parseInt(""+j[3],10),alpha:parseFloat(""+h[4])>1?parseFloat(""+h[4])/100:parseFloat(""+h[4])}}throw new c(5)}o(P,"parseToRgb");function ce(r){var e=r.red/255,t=r.green/255,a=r.blue/255,n=Math.max(e,t,a),s=Math.min(e,t,a),i=(n+s)/2;if(n===s)return r.alpha!==void 0?{hue:0,saturation:0,lightness:i,alpha:r.alpha}:{hue:0,saturation:0,lightness:i};var u,p=n-s,l=i>.5?p/(2-n-s):p/(n+s);switch(n){case e:u=(t-a)/p+(t=1?I(r,e,t):"rgba("+C(r,e,t)+","+a+")";if(typeof r=="object"&&e===void 0&&t===void 0&&a===void 0)return r.alpha>=1?I(r.hue,r.saturation,r.lightness):"rgba("+C(r.hue,r.saturation,r.lightness)+","+r.alpha+")";throw new c(2)}o(he,"hsla");function L(r,e,t){if(typeof r=="number"&&typeof e=="number"&&typeof t=="number")return Y("#"+x(r)+x(e)+x(t));if(typeof r=="object"&&e===void 0&&t===void 0)return Y("#"+x(r.red)+x(r.green)+x(r.blue));throw new c(6)}o(L,"rgb");function F(r,e,t,a){if(typeof r=="string"&&typeof e=="number"){var n=P(r);return"rgba("+n.red+","+n.green+","+n.blue+","+e+")"}else{if(typeof r=="number"&&typeof e=="number"&&typeof t=="number"&&typeof a=="number")return a>=1?L(r,e,t):"rgba("+r+","+e+","+t+","+a+")";if(typeof r=="object"&&e===void 0&&t===void 0&&a===void 0)return r.alpha>=1?L(r.red,r.green,r.blue):"rgba("+r.red+","+r.green+","+r.blue+","+r.alpha+")"}throw new c(7)}o(F,"rgba");var Qe=o(function(e){return typeof e.red=="number"&&typeof e.green=="number"&&typeof e.blue=="number"&&(typeof e.alpha!="number"||typeof e.alpha>"u")},"isRgb"),Xe=o(function(e){return typeof e.red=="number"&&typeof e.green=="number"&&typeof e.blue=="number"&&typeof e.alpha=="number"},"isRgba"),Ve=o(function(e){return typeof e.hue=="number"&&typeof e.saturation=="number"&&typeof e.lightness=="number"&&(typeof e.alpha!="number"||typeof e.alpha>"u")},"isHsl"),er=o(function(e){return typeof e.hue=="number"&&typeof e.saturation=="number"&&typeof e.lightness=="number"&&typeof e.alpha=="number"},"isHsla");function v(r){if(typeof r!="object")throw new c(8);if(Xe(r))return F(r);if(Qe(r))return L(r);if(er(r))return he(r);if(Ve(r))return ge(r);throw new c(8)}o(v,"toColorString");function K(r,e,t){return o(function(){var n=t.concat(Array.prototype.slice.call(arguments));return n.length>=e?r.apply(this,n):K(r,e,n)},"fn")}o(K,"curried");function m(r){return K(r,r.length,[])}o(m,"curry");function be(r,e){if(e==="transparent")return e;var t=y(e);return v(f({},t,{hue:t.hue+parseFloat(r)}))}o(be,"adjustHue");var Xa=m(be);function O(r,e,t){return Math.max(r,Math.min(e,t))}o(O,"guard");function ye(r,e){if(e==="transparent")return e;var t=y(e);return v(f({},t,{lightness:O(0,1,t.lightness-parseFloat(r))}))}o(ye,"darken");var Va=m(ye);function ve(r,e){if(e==="transparent")return e;var t=y(e);return v(f({},t,{saturation:O(0,1,t.saturation-parseFloat(r))}))}o(ve,"desaturate");var en=m(ve);function _e(r,e){if(e==="transparent")return e;var t=y(e);return v(f({},t,{lightness:O(0,1,t.lightness+parseFloat(r))}))}o(_e,"lighten");var rn=m(_e);function xe(r,e,t){if(e==="transparent")return t;if(t==="transparent")return e;if(r===0)return t;var a=P(e),n=f({},a,{alpha:typeof a.alpha=="number"?a.alpha:1}),s=P(t),i=f({},s,{alpha:typeof s.alpha=="number"?s.alpha:1}),u=n.alpha-i.alpha,p=parseFloat(r)*2-1,l=p*u===-1?p:p+u,b=1+p*u,g=(l/b+1)/2,h=1-g,E={red:Math.floor(n.red*g+i.red*h),green:Math.floor(n.green*g+i.green*h),blue:Math.floor(n.blue*g+i.blue*h),alpha:n.alpha*parseFloat(r)+i.alpha*(1-parseFloat(r))};return F(E)}o(xe,"mix");var rr=m(xe),Se=rr;function ke(r,e){if(e==="transparent")return e;var t=P(e),a=typeof t.alpha=="number"?t.alpha:1,n=f({},t,{alpha:O(0,1,(a*100+parseFloat(r)*100)/100)});return F(n)}o(ke,"opacify");var tn=m(ke);function we(r,e){if(e==="transparent")return e;var t=y(e);return v(f({},t,{saturation:O(0,1,t.saturation+parseFloat(r))}))}o(we,"saturate");var an=m(we);function Te(r,e){return e==="transparent"?e:v(f({},y(e),{hue:parseFloat(r)}))}o(Te,"setHue");var nn=m(Te);function Pe(r,e){return e==="transparent"?e:v(f({},y(e),{lightness:parseFloat(r)}))}o(Pe,"setLightness");var on=m(Pe);function Oe(r,e){return e==="transparent"?e:v(f({},y(e),{saturation:parseFloat(r)}))}o(Oe,"setSaturation");var sn=m(Oe);function Re(r,e){return e==="transparent"?e:Se(parseFloat(r),"rgb(0, 0, 0)",e)}o(Re,"shade");var un=m(Re);function Ce(r,e){return e==="transparent"?e:Se(parseFloat(r),"rgb(255, 255, 255)",e)}o(Ce,"tint");var pn=m(Ce);function Ee(r,e){if(e==="transparent")return e;var t=P(e),a=typeof t.alpha=="number"?t.alpha:1,n=f({},t,{alpha:O(0,1,+(a*100-parseFloat(r)*100).toFixed(2)/100)});return F(n)}o(Ee,"transparentize");var tr=m(Ee),ar=tr,nr=_.div(te,({theme:r})=>({backgroundColor:r.base==="light"?"rgba(0,0,0,.01)":"rgba(255,255,255,.01)",borderRadius:r.appBorderRadius,border:`1px dashed ${r.appBorderColor}`,display:"flex",alignItems:"center",justifyContent:"center",padding:20,margin:"25px 0 40px",color:ar(.3,r.color.defaultText),fontSize:r.typography.size.s2})),or=o(r=>d.createElement(nr,{...r,className:"docblock-emptyblock sb-unstyled"}),"EmptyBlock"),sr=_(re)(({theme:r})=>({fontSize:`${r.typography.size.s2-1}px`,lineHeight:"19px",margin:"25px 0 40px",borderRadius:r.appBorderRadius,boxShadow:r.base==="light"?"rgba(0, 0, 0, 0.10) 0 1px 3px 0":"rgba(0, 0, 0, 0.20) 0 2px 5px 0","pre.prismjs":{padding:20,background:"inherit"}})),ir=_.div(({theme:r})=>({background:r.background.content,borderRadius:r.appBorderRadius,border:`1px solid ${r.appBorderColor}`,boxShadow:r.base==="light"?"rgba(0, 0, 0, 0.10) 0 1px 3px 0":"rgba(0, 0, 0, 0.20) 0 2px 5px 0",margin:"25px 0 40px",padding:"20px 20px 20px 22px"})),z=_.div(({theme:r})=>({animation:`${r.animation.glow} 1.5s ease-in-out infinite`,background:r.appBorderColor,height:17,marginTop:1,width:"60%",[`&:first-child${H}`]:{margin:0}})),ur=o(()=>d.createElement(ir,null,d.createElement(z,null),d.createElement(z,{style:{width:"80%"}}),d.createElement(z,{style:{width:"30%"}}),d.createElement(z,{style:{width:"80%"}})),"SourceSkeleton"),Ie=o(({isLoading:r,error:e,language:t,code:a,dark:n,format:s=!0,...i})=>{let{typography:u}=N();if(r)return d.createElement(ur,null);if(e)return d.createElement(or,null,e);let p=d.createElement(sr,{bordered:!0,copyable:!0,format:s,language:t??"jsx",className:"docblock-source sb-unstyled",...i},a);if(typeof n>"u")return p;let l=n?q.dark:q.light;return d.createElement(ae,{theme:ne({...l,fontCode:u.fonts.mono,fontBase:u.fonts.base})},p)},"Source");var gn=__STORYBOOK_API__,{ActiveTabs:hn,Consumer:bn,ManagerContext:yn,Provider:vn,RequestResponseError:_n,addons:J,combineParameters:xn,controlOrMetaKey:Sn,controlOrMetaSymbol:kn,eventMatchesShortcut:wn,eventToShortcut:Tn,experimental_MockUniversalStore:Pn,experimental_UniversalStore:On,experimental_getStatusStore:Rn,experimental_getTestProviderStore:Cn,experimental_requestResponse:En,experimental_useStatusStore:In,experimental_useTestProviderStore:Fn,experimental_useUniversalStore:jn,internal_fullStatusStore:Hn,internal_fullTestProviderStore:Nn,internal_universalStatusStore:zn,internal_universalTestProviderStore:An,isMacLike:Mn,isShortcutTaken:Bn,keyToSymbol:Dn,merge:Ln,mockChannel:qn,optionOrAltSymbol:$n,shortcutMatchesShortcut:Un,shortcutToHumanString:Wn,types:Fe,useAddonState:Yn,useArgTypes:Gn,useArgs:Kn,useChannel:je,useGlobalTypes:Jn,useGlobals:Zn,useParameter:He,useSharedState:Qn,useStoryPrepared:Xn,useStorybookApi:Vn,useStorybookState:eo}=__STORYBOOK_API__;var Z="storybook/docs",pr=`${Z}/panel`,Ne="docs",ze=`${Z}/snippet-rendered`;J.register(Z,r=>{J.add(pr,{title:"Code",type:Fe.PANEL,paramKey:Ne,disabled:o(e=>!e?.docs?.codePanel,"disabled"),match:o(({viewMode:e})=>e==="story","match"),render:o(({active:e})=>{let t=r.getChannel(),a=r.getCurrentStoryData(),n=t?.last(ze)?.[0],[s,i]=V({source:n?.source,format:n?.format??void 0}),u=He(Ne,{source:{code:""},theme:"dark"});X(()=>{i({source:void 0,format:void 0})},[a?.id]),je({[ze]:({source:b,format:g})=>{i({source:b,format:g})}});let l=N().base!=="light";return d.createElement(ee,{active:!!e},d.createElement(lr,null,d.createElement(Ie,{...u.source,code:u.source?.code||s.source||u.source?.originalSource,format:s.format,dark:l})))},"render")})});var lr=_.div(()=>({height:"100%",[`> :first-child${H}`]:{margin:0,height:"100%",boxShadow:"none"}}));})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/links-2/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var e="storybook/links";var a={NAVIGATE:`${e}/navigate`,REQUEST:`${e}/request`,RECEIVE:`${e}/receive`};var f=__STORYBOOK_API__,{ActiveTabs:k,Consumer:T,ManagerContext:O,Provider:U,RequestResponseError:E,addons:n,combineParameters:A,controlOrMetaKey:P,controlOrMetaSymbol:x,eventMatchesShortcut:R,eventToShortcut:h,experimental_MockUniversalStore:j,experimental_UniversalStore:I,experimental_getStatusStore:D,experimental_getTestProviderStore:M,experimental_requestResponse:C,experimental_useStatusStore:N,experimental_useTestProviderStore:K,experimental_useUniversalStore:B,internal_fullStatusStore:V,internal_fullTestProviderStore:Y,internal_universalStatusStore:q,internal_universalTestProviderStore:G,isMacLike:L,isShortcutTaken:$,keyToSymbol:H,merge:Q,mockChannel:w,optionOrAltSymbol:z,shortcutMatchesShortcut:F,shortcutToHumanString:J,types:W,useAddonState:X,useArgTypes:Z,useArgs:ee,useChannel:re,useGlobalTypes:te,useGlobals:oe,useParameter:se,useSharedState:ae,useStoryPrepared:ne,useStorybookApi:de,useStorybookState:ie}=__STORYBOOK_API__;n.register(e,r=>{r.on(a.REQUEST,({kind:d,name:i})=>{let _=r.storyId(d,i);r.emit(a.RECEIVE,_)})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/storybook-4/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var J=Object.create;var U=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var N=Object.getPrototypeOf,$=Object.prototype.hasOwnProperty;var I=(t,e)=>()=>(t&&(e=t(t=0)),e);var P=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var ee=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of X(e))!$.call(t,a)&&a!==n&&U(t,a,{get:()=>e[a],enumerable:!(r=Y(e,a))||r.enumerable});return t};var te=(t,e,n)=>(n=t!=null?J(N(t)):{},ee(e||!t||!t.__esModule?U(n,"default",{value:t,enumerable:!0}):n,t));var d=I(()=>{});var g=I(()=>{});var p=I(()=>{});var q=P(C=>{"use strict";d();g();p();Object.defineProperty(C,"__esModule",{value:!0});C.default=void 0;var ne=function(){for(var e=arguments.length,n=new Array(e),r=0;r"u"&&(window.dataLayer=window.dataLayer||[],window.gtag=function(){window.dataLayer.push(arguments)}),(a=window).gtag.apply(a,n)}},re=ne;C.default=re});var D=P(G=>{"use strict";d();g();p();Object.defineProperty(G,"__esModule",{value:!0});G.default=le;var ae=/^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i;function oe(t){return t.toString().trim().replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g,function(e,n,r){return n>0&&n+e.length!==r.length&&e.search(ae)>-1&&r.charAt(n-2)!==":"&&(r.charAt(n+e.length)!=="-"||r.charAt(n-1)==="-")&&r.charAt(n-1).search(/[^\s-]/)<0?e.toLowerCase():e.substr(1).search(/[A-Z]|\../)>-1?e:e.charAt(0).toUpperCase()+e.substr(1)})}function ie(t){return typeof t=="string"&&t.indexOf("@")!==-1}var ue="REDACTED (Potential Email Address)";function se(t){return ie(t)?(console.warn("This arg looks like an email address, redacting."),ue):t}function le(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"",e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,r=t||"";return e&&(r=oe(t)),n&&(r=se(r)),r}});var F=P(v=>{"use strict";d();g();p();Object.defineProperty(v,"__esModule",{value:!0});v.default=v.GA4=void 0;var ce=z(q()),T=z(D()),fe=["eventCategory","eventAction","eventLabel","eventValue","hitType"],de=["title","location"],ge=["page","hitType"];function z(t){return t&&t.__esModule?t:{default:t}}function j(t,e){if(t==null)return{};var n=pe(t,e),r,a;if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(a=0;a=0)&&Object.prototype.propertyIsEnumerable.call(t,r)&&(n[r]=t[r])}return n}function pe(t,e){if(t==null)return{};var n={},r=Object.keys(t),a,o;for(o=0;o=0)&&(n[a]=t[a]);return n}function _(t){"@babel/helpers - typeof";return _=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_(t)}function y(t){return _e(t)||ye(t)||x(t)||me()}function me(){throw new TypeError(`Invalid attempt to spread non-iterable instance. 3 | In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function ye(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}function _e(t){if(Array.isArray(t))return E(t)}function R(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable})),n.push.apply(n,r)}return n}function m(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n2&&arguments[2]!==void 0?arguments[2]:"https://www.googletagmanager.com/gtag/js";if(!(typeof window>"u"||typeof document>"u")&&!e._hasLoadedGA){var o=document.createElement("script");o.async=!0,o.src="".concat(a,"?id=").concat(n),r&&o.setAttribute("nonce",r),document.body.appendChild(o),window.dataLayer=window.dataLayer||[],window.gtag=function(){window.dataLayer.push(arguments)},e._hasLoadedGA=!0}}),l(this,"_toGtagOptions",function(n){if(n){var r={cookieUpdate:"cookie_update",cookieExpires:"cookie_expires",cookieDomain:"cookie_domain",cookieFlags:"cookie_flags",userId:"user_id",clientId:"client_id",anonymizeIp:"anonymize_ip",contentGroup1:"content_group1",contentGroup2:"content_group2",contentGroup3:"content_group3",contentGroup4:"content_group4",contentGroup5:"content_group5",allowAdFeatures:"allow_google_signals",allowAdPersonalizationSignals:"allow_ad_personalization_signals",nonInteraction:"non_interaction",page:"page_path",hitCallback:"event_callback"},a=Object.entries(n).reduce(function(o,i){var s=ve(i,2),u=s[0],c=s[1];return r[u]?o[r[u]]=c:o[u]=c,o},{});return a}}),l(this,"initialize",function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(!n)throw new Error("Require GA_MEASUREMENT_ID");var a=typeof n=="string"?[{trackingId:n}]:n;e._currentMeasurementId=a[0].trackingId;var o=r.gaOptions,i=r.gtagOptions,s=r.nonce,u=r.testMode,c=u===void 0?!1:u,f=r.gtagUrl;if(e._testMode=c,c||e._loadGA(e._currentMeasurementId,s,f),e.isInitialized||(e._gtag("js",new Date),a.forEach(function(S){var M=m(m(m({},e._toGtagOptions(m(m({},o),S.gaOptions))),i),S.gtagOptions);Object.keys(M).length?e._gtag("config",S.trackingId,M):e._gtag("config",S.trackingId)})),e.isInitialized=!0,!c){var A=y(e._queueGtag);for(e._queueGtag=[],e._isQueuing=!1;A.length;){var L=A.shift();e._gtag.apply(e,y(L)),L[0]==="get"&&(e._isQueuing=!0)}}}),l(this,"set",function(n){if(!n){console.warn("`fieldsObject` is required in .set()");return}if(_(n)!=="object"){console.warn("Expected `fieldsObject` arg to be an Object");return}Object.keys(n).length===0&&console.warn("empty `fieldsObject` given to .set()"),e._gaCommand("set",n)}),l(this,"_gaCommandSendEvent",function(n,r,a,o,i){e._gtag("event",r,m(m({event_category:n,event_label:a,value:o},i&&{non_interaction:i.nonInteraction}),e._toGtagOptions(i)))}),l(this,"_gaCommandSendEventParameters",function(){for(var n=arguments.length,r=new Array(n),a=0;a1?r-1:0),o=1;o{"use strict";d();g();p();function k(t){"@babel/helpers - typeof";return k=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},k(t)}Object.defineProperty(b,"__esModule",{value:!0});b.default=b.ReactGAImplementation=void 0;var B=Ce(F());function K(t){if(typeof WeakMap!="function")return null;var e=new WeakMap,n=new WeakMap;return(K=function(a){return a?n:e})(t)}function Ce(t,e){if(!e&&t&&t.__esModule)return t;if(t===null||k(t)!=="object"&&typeof t!="function")return{default:t};var n=K(e);if(n&&n.has(t))return n.get(t);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in t)if(o!=="default"&&Object.prototype.hasOwnProperty.call(t,o)){var i=a?Object.getOwnPropertyDescriptor(t,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=t[o]}return r.default=t,n&&n.set(t,r),r}var Ie=B.GA4;b.ReactGAImplementation=Ie;var Ge=B.default;b.default=Ge});d();g();p();d();g();p();var Z=te(H(),1);Z.default.initialize("G-V30ERZKLWJ",{gtagOptions:{anonymizeIp:!0,content_group:"storybook"}});})(); 5 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 6 | -------------------------------------------------------------------------------- /docs/sb-addons/vueless-storybook-dark-mode-3/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- 1 | Bundled license information: 2 | 3 | lodash-es/lodash.default.js: 4 | lodash-es/lodash.js: 5 | /** 6 | * @license 7 | * Lodash (Custom Build) 8 | * Build: `lodash modularize exports="es" -o ./` 9 | * Copyright OpenJS Foundation and other contributors 10 | * Released under MIT license 11 | * Based on Underscore.js 1.8.3 12 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 13 | */ 14 | -------------------------------------------------------------------------------- /docs/sb-common-assets/favicon-wrapper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 32 | 34 | 36 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/sb-common-assets/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/sb-common-assets/nunito-sans-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/sb-common-assets/nunito-sans-bold.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/sb-common-assets/nunito-sans-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/docs/sb-common-assets/nunito-sans-regular.woff2 -------------------------------------------------------------------------------- /docs/sb-manager/globals.js: -------------------------------------------------------------------------------- 1 | import "../_browser-chunks/chunk-MM7DTO55.js"; 2 | 3 | // src/manager/globals/globals.ts 4 | var globalsNameReferenceMap = { 5 | react: "__REACT__", 6 | "react-dom": "__REACT_DOM__", 7 | "react-dom/client": "__REACT_DOM_CLIENT__", 8 | "@storybook/icons": "__STORYBOOK_ICONS__", 9 | "storybook/manager-api": "__STORYBOOK_API__", 10 | "storybook/test": "__STORYBOOK_TEST__", 11 | "storybook/theming": "__STORYBOOK_THEMING__", 12 | "storybook/theming/create": "__STORYBOOK_THEMING_CREATE__", 13 | "storybook/internal/channels": "__STORYBOOK_CHANNELS__", 14 | "storybook/internal/client-logger": "__STORYBOOK_CLIENT_LOGGER__", 15 | "storybook/internal/components": "__STORYBOOK_COMPONENTS__", 16 | "storybook/internal/core-events": "__STORYBOOK_CORE_EVENTS__", 17 | "storybook/internal/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__", 18 | "storybook/internal/router": "__STORYBOOK_ROUTER__", 19 | "storybook/internal/types": "__STORYBOOK_TYPES__" 20 | }; 21 | var globalPackages = Object.keys(globalsNameReferenceMap); 22 | export { 23 | globalPackages, 24 | globalsNameReferenceMap 25 | }; 26 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import react from 'eslint-plugin-react' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import storybook from 'eslint-plugin-storybook' 5 | import { defineConfig } from 'eslint/config' 6 | import globals from 'globals' 7 | import typescript from 'typescript-eslint' 8 | 9 | export default defineConfig( 10 | eslint.configs.recommended, 11 | typescript.configs.strictTypeChecked, 12 | typescript.configs.stylisticTypeChecked, 13 | react.configs.flat.recommended, 14 | react.configs.flat['jsx-runtime'], 15 | reactHooks.configs.flat.recommended, 16 | storybook.configs['flat/recommended'], 17 | { 18 | rules: { 19 | 'no-console': 'error', 20 | '@typescript-eslint/array-type': ['error', { default: 'generic' }], 21 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 22 | '@typescript-eslint/non-nullable-type-assertion-style': 'off', 23 | '@typescript-eslint/restrict-template-expressions': 'off', 24 | 'react/no-unescaped-entities': 'off', 25 | }, 26 | languageOptions: { 27 | globals: { 28 | ...globals.browser, 29 | ...globals.node, 30 | }, 31 | parserOptions: { 32 | projectService: true, 33 | tsconfigRootDir: import.meta.dirname, 34 | }, 35 | }, 36 | settings: { 37 | react: { 38 | version: 'detect', 39 | }, 40 | }, 41 | }, 42 | { 43 | // Note: there must be no other properties in this object 44 | ignores: ['build/', 'coverage/', 'docs/', 'examples/', 'rollup.config.mjs'], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /examples/customization.tsx: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /examples/event-handlers.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement } from 'react' 2 | 3 | 6 | cloneElement(block, { 7 | onClick: () => { 8 | alert(JSON.stringify(activity)) 9 | }, 10 | onMouseEnter: () => { 11 | console.log('on mouse enter') 12 | }, 13 | }) 14 | /> 15 | -------------------------------------------------------------------------------- /examples/labels-shape.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityCalendar, 3 | Props as CalendarProps, 4 | } from 'react-activity-calendar' 5 | 6 | // Shape of `labels` property (default values). 7 | // All properties are optional. 8 | const labels = { 9 | months: [ 10 | 'Jan', 11 | 'Feb', 12 | 'Mar', 13 | 'Apr', 14 | 'May', 15 | 'Jun', 16 | 'Jul', 17 | 'Aug', 18 | 'Sep', 19 | 'Oct', 20 | 'Nov', 21 | 'Dec', 22 | ], 23 | weekdays: [ 24 | 'Sun', // Sunday first! 25 | 'Mon', 26 | 'Tue', 27 | 'Wed', 28 | 'Thu', 29 | 'Fri', 30 | 'Sat', 31 | ], 32 | totalCount: '{{count}} activities in {{year}}', 33 | legend: { 34 | less: 'Less', 35 | more: 'More', 36 | }, 37 | } satisfies CalendarProps['labels'] 38 | 39 | -------------------------------------------------------------------------------- /examples/labels.tsx: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /examples/ref.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | const calendarRef = useRef(null) 4 | 5 | if (calendar.current) { 6 | console.log(calendarRef.current) 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/themes-explicit.tsx: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/themes.tsx: -------------------------------------------------------------------------------- 1 | const explicitTheme: ThemeInput = { 2 | light: ['#f0f0f0', '#c4edde', '#7ac7c4', '#f73859', '#384259'], 3 | dark: ['#383838', '#4D455D', '#7DB9B6', '#F5E9CF', '#E96479'], 4 | } 5 | 6 | 7 | 8 | const minimalTheme: ThemeInput = { 9 | light: ['hsl(0, 0%, 92%)', 'rebeccapurple'], 10 | // for `dark` the default theme will be used 11 | } 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/tooltips-config.tsx: -------------------------------------------------------------------------------- 1 | `${activity.level} activities on ${activity.date}`, 6 | placement: 'right', 7 | offset: 6, 8 | hoverRestMs: 300, 9 | transitionStyles: { 10 | duration: 100, 11 | common: { fontFamily: 'monospace' }, 12 | }, 13 | withArrow: true, 14 | }, 15 | }} 16 | /> 17 | -------------------------------------------------------------------------------- /examples/tooltips.tsx: -------------------------------------------------------------------------------- 1 | `${activity.level} activities on ${activity.date}` 6 | }, 7 | colorLegend: { 8 | text: level => `Activity level ${level + 1}` 9 | }, 10 | }} 11 | /> 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-activity-calendar", 3 | "version": "3.0.1", 4 | "description": "React component to display activity data in calendar", 5 | "author": "Jonathan Gruber ", 6 | "license": "MIT", 7 | "homepage": "https://grubersjoe.github.io/react-activity-calendar/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/grubersjoe/react-activity-calendar.git" 11 | }, 12 | "exports": { 13 | ".": "./build/index.js", 14 | "./tooltips.css": "./build/tooltips.css" 15 | }, 16 | "type": "module", 17 | "types": "build/index.d.ts", 18 | "files": [ 19 | "build", 20 | "src", 21 | "README.md", 22 | "tsconfig.json" 23 | ], 24 | "scripts": { 25 | "build": "rollup -c", 26 | "build:storybook": "storybook build -o docs/ && touch docs/.nojekyll", 27 | "bundlesize": "pnpm install && EXTERNAL_DEPS=false pnpm build && open bundle.html", 28 | "check": "pnpm install --frozen-lockfile && pnpx prettier -c . && pnpm exec tsc && pnpm lint && pnpm test:unit", 29 | "dev": "storybook dev -p 9000", 30 | "format": "prettier --write .", 31 | "lint": "eslint .", 32 | "postbuild": "dts-bundle-generator src/index.tsx -o build/index.d.ts --no-check --no-banner --external-imports react date-fns @floating-ui/react", 33 | "prebuild": "rm -rf build/*", 34 | "prepare": "husky", 35 | "prepublishOnly": "pnpm check && pnpm build", 36 | "react-docgen": "pnpm dlx @react-docgen/cli src/components/ActivityCalendar.tsx | jq '.[\"src/components/ActivityCalendar.tsx\"][0]'", 37 | "start": "rollup -c -w", 38 | "test": "pnpm test:unit && pnpm test:visual", 39 | "test:unit": "jest", 40 | "test:visual": "docker compose run -p 9001:9001 --rm playwright bash -c 'npm install --silent --no-package-lock && npx playwright test'", 41 | "test:visual:update": "docker compose run --rm playwright bash -c 'npm install --silent --no-package-lock && npx playwright test --update-snapshots'" 42 | }, 43 | "dependencies": { 44 | "@floating-ui/react": "^0.27.12", 45 | "date-fns": "^4.1.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.27.7", 49 | "@babel/preset-env": "^7.27.2", 50 | "@babel/preset-react": "^7.27.1", 51 | "@babel/preset-typescript": "^7.27.1", 52 | "@eslint/js": "^9.30.0", 53 | "@ianvs/prettier-plugin-sort-imports": "^4.4.2", 54 | "@jest/globals": "^30.0.3", 55 | "@jest/types": "^30.0.1", 56 | "@playwright/test": "^1.56.1", 57 | "@rollup/plugin-babel": "^6.1.0", 58 | "@rollup/plugin-commonjs": "^29.0.0", 59 | "@rollup/plugin-node-resolve": "^16.0.3", 60 | "@rollup/plugin-replace": "^6.0.2", 61 | "@storybook/addon-docs": "^10.0.7", 62 | "@storybook/addon-links": "^10.0.7", 63 | "@storybook/react-vite": "^10.0.7", 64 | "@types/react": "^19.2.5", 65 | "@vitejs/plugin-react": "^5.1.1", 66 | "@vueless/storybook-dark-mode": "^10.0.2", 67 | "dts-bundle-generator": "^9.5.1", 68 | "eslint": "^9.30.0", 69 | "eslint-plugin-react": "^7.37.5", 70 | "eslint-plugin-react-hooks": "^7.0.0", 71 | "eslint-plugin-storybook": "10.0.8", 72 | "globals": "^16.2.0", 73 | "husky": "^9.1.7", 74 | "jest": "^30.0.3", 75 | "jest-environment-jsdom": "^30.0.2", 76 | "prettier": "^3.6.2", 77 | "prism-react-renderer": "^2.4.1", 78 | "react": "^19.1.0", 79 | "react-docgen": "^8.0.2", 80 | "react-ga4": "^2.1.0", 81 | "rollup": "^4.44.1", 82 | "rollup-plugin-copy": "^3.5.0", 83 | "rollup-plugin-filesize": "^10.0.0", 84 | "rollup-plugin-visualizer": "^6.0.3", 85 | "sass-embedded": "^1.93.2", 86 | "storybook": "^10.0.7", 87 | "typescript": "^5.8.3", 88 | "typescript-eslint": "^8.46.4", 89 | "vite": "^7.0.0" 90 | }, 91 | "peerDependencies": { 92 | "react": "^18.0.0 || ^19.0.0" 93 | }, 94 | "pnpm": { 95 | "overrides": { 96 | "react": "^19.0.0", 97 | "react-dom": "^19.0.0" 98 | }, 99 | "onlyBuiltDependencies": [ 100 | "esbuild", 101 | "unrs-resolver" 102 | ] 103 | }, 104 | "jest": { 105 | "testEnvironment": "jsdom", 106 | "testMatch": [ 107 | "/src/**/*.test.ts" 108 | ] 109 | }, 110 | "browserslist": [ 111 | "last 1 chrome version", 112 | "last 1 firefox version", 113 | "last 1 safari version" 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | // https://playwright.dev/docs/test-configuration. 4 | export default defineConfig({ 5 | testDir: './tests', 6 | outputDir: '.playwright', 7 | fullyParallel: true, 8 | forbidOnly: !!process.env.CI, 9 | retries: process.env.CI ? 1 : 0, 10 | reporter: [ 11 | ['list'], 12 | [ 13 | 'html', 14 | { 15 | outputFolder: '.playwright-report', 16 | host: '0.0.0.0', 17 | port: 9001, 18 | }, 19 | ], 20 | ], 21 | use: { 22 | baseURL: 'http://localhost:9000', 23 | trace: 'on-first-retry', 24 | }, 25 | projects: [ 26 | { 27 | name: 'chromium', 28 | use: { ...devices['Desktop Chrome'] }, 29 | }, 30 | { 31 | name: 'firefox', 32 | use: { ...devices['Desktop Firefox'] }, 33 | }, 34 | { 35 | name: 'webkit', 36 | use: { ...devices['Desktop Safari'] }, 37 | }, 38 | ], 39 | webServer: { 40 | command: 'npx storybook dev -p 9000 --ci', 41 | url: 'http://localhost:9000', 42 | reuseExistingServer: !process.env.CI, 43 | stdout: 'ignore', 44 | stderr: 'pipe', 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | import replace from '@rollup/plugin-replace' 5 | import copy from 'rollup-plugin-copy' 6 | import filesize from 'rollup-plugin-filesize' 7 | import { visualizer } from 'rollup-plugin-visualizer' 8 | 9 | const extensions = ['.ts', '.tsx'] 10 | 11 | // Pass EXTERNAL_DEPS=false to bundle this project including all dependencies. 12 | // Useful to analyze the bundle size. 13 | const useExternal = process.env.EXTERNAL_DEPS?.toLowerCase() !== 'false' 14 | 15 | export default { 16 | input: 'src/index.tsx', 17 | output: { 18 | dir: 'build', 19 | format: 'es', 20 | chunkFileNames: 'chunks/[name]-[hash].js', 21 | sourcemap: true, 22 | exports: 'named', 23 | // Use 'auto' instead of 'default' to support more environments. 24 | // https://rollupjs.org/guide/en/#outputinterop 25 | interop: 'auto', 26 | // Rollup does not support this React Server Components directive yet: 27 | // https://github.com/rollup/rollup/issues/4699 28 | banner: `'use client';`, 29 | }, 30 | external: useExternal ? ['react', 'react/jsx-runtime', 'date-fns', '@floating-ui/react'] : [], 31 | plugins: [ 32 | replace({ 33 | preventAssignment: true, // recommended to set this to true, will be default in the next major version 34 | 'process.env.NODE_ENV': JSON.stringify('production'), 35 | }), 36 | ...(useExternal ? [] : [commonjs()]), 37 | babel({ 38 | extensions, 39 | exclude: 'node_modules/**', 40 | babelHelpers: 'bundled', 41 | }), 42 | resolve({ 43 | extensions, 44 | }), 45 | copy({ 46 | targets: [{ src: 'src/styles/tooltips.css', dest: 'build/' }], 47 | }), 48 | filesize(), 49 | visualizer({ 50 | filename: 'bundle.html', 51 | }), 52 | ], 53 | onwarn(warning, warn) { 54 | if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('use client')) { 55 | return // ignore the error for now 56 | } 57 | warn(warning) 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/screenshot.png -------------------------------------------------------------------------------- /src/components/ActivityCalendar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | cloneElement, 3 | useEffect, 4 | useMemo, 5 | useRef, 6 | type ForwardedRef, 7 | type ReactElement, 8 | } from 'react' 9 | import LinkTo from '@storybook/addon-links/react' 10 | import type { Meta, StoryObj } from '@storybook/react-vite' 11 | import { useDarkMode } from '@vueless/storybook-dark-mode' 12 | import Container from '../../.storybook/components/Container' 13 | import exampleCustomization from '../../examples/customization?raw' 14 | import exampleEventHandlers from '../../examples/event-handlers?raw' 15 | import exampleLabelsShape from '../../examples/labels-shape?raw' 16 | import exampleLabels from '../../examples/labels?raw' 17 | import exampleRef from '../../examples/ref?raw' 18 | import exampleThemeExplicit from '../../examples/themes-explicit?raw' 19 | import exampleTheme from '../../examples/themes?raw' 20 | import exampleTooltipsConfig from '../../examples/tooltips-config?raw' 21 | import exampleTooltips from '../../examples/tooltips?raw' 22 | import exampleTooltipsCSS from '../../src/styles/tooltips.css?raw' 23 | import { Source } from '../docs/Source' 24 | import { generateTestData } from '../lib/calendar' 25 | import type { Theme } from '../types' 26 | import { ActivityCalendar, type Props } from './ActivityCalendar' 27 | import '../styles/tooltips.css' 28 | 29 | type Story = StoryObj 30 | 31 | const meta: Meta> = { 32 | title: 'React Activity Calendar', 33 | component: ActivityCalendar, 34 | argTypes: { 35 | data: { 36 | control: false, 37 | }, 38 | blockMargin: { 39 | control: { type: 'range', min: 0, max: 20 }, 40 | }, 41 | blockRadius: { 42 | control: { type: 'range', min: 0, max: 20 }, 43 | }, 44 | blockSize: { 45 | control: { type: 'range', min: 4, max: 30, step: 1 }, 46 | }, 47 | colorScheme: { 48 | control: false, 49 | }, 50 | fontSize: { 51 | control: { type: 'range', min: 6, max: 32, step: 2 }, 52 | }, 53 | maxLevel: { 54 | control: { type: 'range', min: 1, max: 10 }, 55 | }, 56 | ref: { 57 | control: false, 58 | }, 59 | showWeekdayLabels: { 60 | control: 'boolean', 61 | }, 62 | style: { 63 | control: false, 64 | }, 65 | tooltips: { 66 | control: false, 67 | }, 68 | weekStart: { 69 | options: [0, 1, 2, 3, 4, 5, 6], 70 | control: { 71 | type: 'select', 72 | labels: { 73 | 0: 'Sunday (0)', 74 | 1: 'Monday (1)', 75 | 2: 'Tuesday (2)', 76 | 3: 'Wednesday (3)', 77 | 4: 'Thursday (4)', 78 | 5: 'Friday (5)', 79 | 6: 'Saturday (6)', 80 | }, 81 | }, 82 | }, 83 | }, 84 | decorators: [ 85 | (Story, { args }) => { 86 | // @ts-expect-error unsure if typing forward refs correctly is possible 87 | args.colorScheme = useDarkMode() ? 'dark' : 'light' 88 | 89 | return 90 | }, 91 | ], 92 | parameters: { 93 | controls: { 94 | sort: 'requiredFirst', 95 | }, 96 | layout: 'centered', 97 | }, 98 | tags: ['autodocs'], 99 | } 100 | 101 | // Storybook does not initialize the controls for some reason 102 | const defaultProps = { 103 | blockMargin: 4, 104 | blockRadius: 2, 105 | blockSize: 12, 106 | fontSize: 14, 107 | loading: false, 108 | maxLevel: 4, 109 | showColorLegend: true, 110 | showMonthLabels: true, 111 | showTotalCount: true, 112 | showWeekdayLabels: false, 113 | weekStart: 0, // Sunday 114 | } satisfies Omit 115 | 116 | export default meta 117 | const explicitTheme: Theme = { 118 | light: ['#f0f0f0', '#c4edde', '#7ac7c4', '#f73859', '#384259'], 119 | dark: ['hsl(0, 0%, 22%)', '#4D455D', '#7DB9B6', '#F5E9CF', '#E96479'], 120 | } 121 | 122 | export const Default: Story = { 123 | args: defaultProps, 124 | render: args => { 125 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 126 | return 127 | }, 128 | parameters: { 129 | docs: { 130 | source: { 131 | code: '', 132 | }, 133 | }, 134 | }, 135 | } 136 | 137 | export const Loading: Story = { 138 | args: { 139 | ...defaultProps, 140 | data: [], 141 | loading: true, 142 | }, 143 | parameters: { 144 | docs: { 145 | source: { 146 | code: '', 147 | }, 148 | }, 149 | }, 150 | } 151 | 152 | export const ActivityLevels: Story = { 153 | args: { 154 | ...defaultProps, 155 | maxLevel: 2, 156 | }, 157 | render: args => { 158 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 159 | 160 | return ( 161 | 162 |

Activity levels

163 | 164 |

165 | Use the{' '} 166 | 167 | maxLevel 168 | {' '} 169 | prop to control the number of activity levels. This value is zero indexed, where 0 170 | represents no activity. maxLevel is 4 by default, so this results in 5 171 | activity levels. Each activity level must be in the interval from 0 to{' '} 172 | maxLevel. 173 |

174 |
175 | ) 176 | }, 177 | parameters: { 178 | docs: { 179 | source: { 180 | code: '', 181 | }, 182 | }, 183 | }, 184 | } 185 | 186 | export const DateRanges: Story = { 187 | args: defaultProps, 188 | render: args => { 189 | const dataLong = useMemo( 190 | () => 191 | generateTestData({ 192 | maxLevel: args.maxLevel, 193 | interval: { 194 | start: new Date(2022, 5, 1), 195 | end: new Date(2023, 4, 31), 196 | }, 197 | }), 198 | [args.maxLevel], 199 | ) 200 | 201 | const dataMedium = useMemo( 202 | () => 203 | generateTestData({ 204 | maxLevel: args.maxLevel, 205 | interval: { 206 | start: new Date(2023, 2, 8), 207 | end: new Date(2023, 7, 1), 208 | }, 209 | }), 210 | [args.maxLevel], 211 | ) 212 | 213 | const dataShort = useMemo( 214 | () => 215 | generateTestData({ 216 | maxLevel: args.maxLevel, 217 | interval: { 218 | start: new Date(2023, 5, 14), 219 | end: new Date(2023, 6, 17), 220 | }, 221 | }), 222 | [args.maxLevel], 223 | ) 224 | 225 | return ( 226 | 227 | 234 | 235 | 236 | 237 | ) 238 | }, 239 | } 240 | 241 | export const ColorThemes: Story = { 242 | args: { 243 | ...defaultProps, 244 | theme: { 245 | light: ['hsl(0, 0%, 92%)', 'rebeccapurple'], 246 | dark: ['hsl(0, 0%, 22%)', 'hsl(225,92%,77%)'], 247 | }, 248 | }, 249 | parameters: { 250 | docs: { 251 | source: { 252 | code: exampleTheme, 253 | }, 254 | }, 255 | }, 256 | render: args => { 257 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 258 | 259 | return ( 260 | 261 |

Color themes

262 | 263 |

264 | Use the{' '} 265 | 266 | theme 267 | {' '} 268 | prop to configure the calendar colors for the light and dark{' '} 269 | color scheme. 270 | Provide all colors per scheme{' '} 271 | 272 | explicitly 273 | {' '} 274 | (5 by default) or set exactly two colors (the lowest and highest intensity) to calculate a 275 | single-hue scale. For explicit themes the color count must match the number of activity 276 | levels, which is controlled by the{' '} 277 | 278 | maxLevel 279 | {' '} 280 | prop. Colors can be specified in any valid CSS format. 281 |

282 |

283 | At least one scheme's colors must be set. If undefined, the default theme is used. By 284 | default, the calendar selects the current system color scheme, but you can enforce a 285 | specific scheme with the{' '} 286 | 287 | colorScheme 288 | {' '} 289 | prop. 290 |

291 | 292 |
293 | ) 294 | }, 295 | } 296 | 297 | export const ExplicitThemes: Story = { 298 | args: { 299 | ...defaultProps, 300 | theme: explicitTheme, 301 | }, 302 | parameters: { 303 | // maxLevel cannot be used for a static explicit theme 304 | controls: { exclude: ['maxLevel'] }, 305 | docs: { 306 | source: { 307 | code: exampleThemeExplicit, 308 | }, 309 | }, 310 | }, 311 | render: args => { 312 | const data = useMemo(() => generateTestData({ maxLevel: 4 }), []) 313 | 314 | return ( 315 | 316 |

Explicit theme

317 |

318 |

319 | See the{' '} 320 | 321 | color themes 322 | {' '} 323 | page for details how to use the theme prop. 324 |

325 | 326 |
327 | ) 328 | }, 329 | } 330 | 331 | export const Customization: Story = { 332 | args: { 333 | ...defaultProps, 334 | blockSize: 14, 335 | blockRadius: 7, 336 | blockMargin: 5, 337 | fontSize: 16, 338 | theme: explicitTheme, 339 | }, 340 | render: args => { 341 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 342 | return 343 | }, 344 | parameters: { 345 | // maxLevel cannot be used for a static explicit theme 346 | controls: { exclude: ['maxLevel'] }, 347 | docs: { 348 | source: { 349 | code: exampleCustomization, 350 | }, 351 | }, 352 | }, 353 | } 354 | 355 | export const EventHandlers: Story = { 356 | args: { 357 | ...defaultProps, 358 | renderBlock: (block, activity) => 359 | cloneElement(block, { 360 | onClick: () => { 361 | alert(JSON.stringify(activity)) 362 | }, 363 | onMouseEnter: () => { 364 | // eslint-disable-next-line no-console 365 | console.log('on mouse enter') 366 | }, 367 | }), 368 | }, 369 | parameters: { 370 | docs: { 371 | source: { 372 | code: exampleEventHandlers, 373 | }, 374 | }, 375 | }, 376 | render: args => { 377 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 378 | 379 | return ( 380 | 381 |

Event Handlers

382 |

383 | Use the{' '} 384 | 385 | renderBlock 386 | {' '} 387 | prop to attach event handlers to the SVG rect elements that represent 388 | calendar days. Click on any block below to see it in action. 389 |

390 | 391 |

392 | Use the React.cloneElement() function to assign the handlers: 393 |

394 | 395 |
396 | ) 397 | }, 398 | } 399 | 400 | export const Tooltips: Story = { 401 | args: { 402 | ...defaultProps, 403 | tooltips: { 404 | activity: { 405 | text: activity => `${activity.level} activities on ${activity.date}`, 406 | }, 407 | colorLegend: { 408 | text: level => `Activity level ${level + 1}`, 409 | }, 410 | }, 411 | }, 412 | render: args => { 413 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 414 | 415 | return ( 416 | 417 |

Tooltips

418 |

419 | Use the{' '} 420 | 421 | tooltips 422 | {' '} 423 | prop to show tooltips when hovering the calendar days or the color legend. Each tooltip's 424 | content is generated by a callback function, which receives either the activity data or 425 | level value of the hovered element. 426 |

427 | 428 |

429 | Tooltips no longer depend on external libraries and are now integrated directly into this 430 | package. Thanks to code-splitting, tooltips only affect your bundle size when you use 431 | them. Tooltips are implemented using the{' '} 432 | Floating UI library as a “headless” component, 433 | meaning they come without predefined styles. This gives you full control over the 434 | appearance: 435 |

436 |
    437 |
  • 438 | Import the default styles provided by this package, or 439 |
  • 440 |
  • Add your own custom CSS.
  • 441 |
442 | 443 | 447 | 448 |

Tooltip configuration

449 |

You can configure the tooltips with the following optional settings:

450 |
    451 |
  • 452 | placement of the 453 | tooltip 454 |
  • 455 |
  • 456 | offset to the element in 457 | pixels (4 by default) 458 |
  • 459 |
  • 460 | the cursor “rest time” in 461 | milliseconds before opening a tooltip (150ms by default) 462 |
  • 463 |
  • 464 | 465 | transition styles 466 | {' '} 467 | to fine-tune CSS animations 468 |
  • 469 |
  • whether to draw an arrow (false by default)
  • 470 |
471 | 472 | `${activity.level} activities on ${activity.date}`, 478 | placement: 'right', 479 | offset: 8, 480 | transitionStyles: { 481 | duration: 100, 482 | common: { fontFamily: 'monospace' }, 483 | }, 484 | hoverRestMs: 300, 485 | withArrow: true, 486 | }, 487 | }} 488 | style={{ margin: '2rem 0' }} 489 | /> 490 |
491 | ) 492 | }, 493 | parameters: { 494 | docs: { 495 | source: { 496 | code: exampleTooltips, 497 | }, 498 | }, 499 | }, 500 | } 501 | 502 | export const WithoutLabels: Story = { 503 | args: { 504 | ...defaultProps, 505 | showMonthLabels: false, 506 | showColorLegend: false, 507 | showTotalCount: false, 508 | }, 509 | render: args => { 510 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 511 | return 512 | }, 513 | parameters: { 514 | docs: { 515 | source: { 516 | code: '', 517 | }, 518 | }, 519 | }, 520 | } 521 | 522 | export const WeekdayLabels: Story = { 523 | args: { 524 | ...defaultProps, 525 | showWeekdayLabels: true, 526 | }, 527 | render: args => { 528 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 529 | return ( 530 | 531 |
532 | Show every second weekday (default) 533 | 534 |
535 | 536 |
537 | Show specific days 538 | 539 |
540 | 541 |
542 | 543 | Show every day 544 | 545 | 550 |
551 |
552 | ) 553 | }, 554 | parameters: { 555 | docs: { 556 | source: { 557 | code: '', 558 | }, 559 | }, 560 | }, 561 | } 562 | 563 | export const LocalizedLabels: Story = { 564 | args: { 565 | ...defaultProps, 566 | showWeekdayLabels: true, 567 | labels: { 568 | months: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], 569 | weekdays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], 570 | totalCount: '{{count}} Aktivitäten in {{year}}', 571 | legend: { 572 | less: 'Weniger', 573 | more: 'Mehr', 574 | }, 575 | }, 576 | }, 577 | parameters: { 578 | docs: { 579 | source: { 580 | code: exampleLabels, 581 | }, 582 | }, 583 | }, 584 | render: args => { 585 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 586 | 587 | return ( 588 | 589 |

Localization

590 |

Example in German:

591 | 592 | 593 |
594 | ) 595 | }, 596 | } 597 | 598 | export const MondayAsWeekStart: Story = { 599 | args: { 600 | ...defaultProps, 601 | weekStart: 1, 602 | }, 603 | render: args => { 604 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 605 | return 606 | }, 607 | parameters: { 608 | docs: { 609 | source: { 610 | code: '', 611 | }, 612 | }, 613 | }, 614 | } 615 | 616 | export const NarrowScreens: Story = { 617 | args: defaultProps, 618 | parameters: { 619 | docs: { 620 | source: { 621 | code: '', 622 | }, 623 | }, 624 | }, 625 | render: args => { 626 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 627 | 628 | return ( 629 |
630 | 631 |
632 | ) 633 | }, 634 | } 635 | 636 | export const ContainerRef: Story = { 637 | args: defaultProps, 638 | parameters: { 639 | docs: { 640 | source: { 641 | code: exampleRef, 642 | }, 643 | }, 644 | }, 645 | render: args => { 646 | const data = useMemo(() => generateTestData({ maxLevel: args.maxLevel }), [args.maxLevel]) 647 | const calendarRef = useRef(null) 648 | 649 | useEffect(() => { 650 | // eslint-disable-next-line no-console 651 | console.log('calendar ref', calendarRef) 652 | }, [calendarRef]) 653 | 654 | return ( 655 | <> 656 | 657 |
658 |

Check the JavaScript console to see the ref logged.

659 | 660 | ) 661 | }, 662 | } 663 | 664 | const Stack = ({ children }: { children: Array }) => ( 665 |
{children}
666 | ) 667 | 668 | const StackHeading = ({ children, code }: { children: string; code?: string }) => ( 669 |
680 | {children} 681 | {code && {code}} 682 |
683 | ) 684 | -------------------------------------------------------------------------------- /src/components/ActivityCalendar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | forwardRef, 5 | Fragment, 6 | lazy, 7 | Suspense, 8 | useEffect, 9 | useState, 10 | type CSSProperties, 11 | type ForwardedRef, 12 | type ReactElement, 13 | } from 'react' 14 | import { getYear, parseISO } from 'date-fns' 15 | import { DEFAULT_LABELS, LABEL_MARGIN, NAMESPACE } from '../constants' 16 | import { useColorScheme } from '../hooks/useColorScheme' 17 | import { loadingAnimationName, useLoadingAnimation } from '../hooks/useLoadingAnimation' 18 | import { usePrefersReducedMotion } from '../hooks/usePrefersReducedMotion' 19 | import { 20 | generateEmptyData, 21 | getClassName, 22 | groupByWeeks, 23 | range, 24 | validateActivities, 25 | } from '../lib/calendar' 26 | import { getMonthLabels, initWeekdayLabels, maxWeekdayLabelWidth } from '../lib/label' 27 | import { createTheme } from '../lib/theme' 28 | import { styles } from '../styles/styles' 29 | import type { 30 | Activity, 31 | BlockElement, 32 | ColorScheme, 33 | DayIndex, 34 | DayName, 35 | Labels, 36 | ThemeInput, 37 | } from '../types' 38 | import { type TooltipConfig } from './Tooltip' 39 | 40 | const Tooltip = lazy(() => import('./Tooltip').then(module => ({ default: module.Tooltip }))) 41 | 42 | export type Props = { 43 | /** 44 | * List of calendar entries. Each `Activity` object requires an ISO 8601 45 | * `date` string in the format `yyyy-MM-dd`, a `count` property with the 46 | * amount of tracked data, and a `level` property in the range `0-maxLevel` 47 | * to specify activity intensity. The `maxLevel` prop defaults to 4. 48 | * 49 | * Dates without corresponding entries are assumed to have no activity. This 50 | * allows you to set arbitrary start and end dates for the calendar by passing 51 | * empty entries as the first and last items. 52 | * 53 | * Example object: 54 | * 55 | * ``` 56 | * { 57 | * date: "2021-02-20", 58 | * count: 16, 59 | * level: 3 60 | * } 61 | * ``` 62 | */ 63 | data: Array 64 | /** 65 | * Margin between blocks in pixels. 66 | */ 67 | blockMargin?: number 68 | /** 69 | * Border radius of blocks in pixels. 70 | */ 71 | blockRadius?: number 72 | /** 73 | * Block size in pixels. 74 | */ 75 | blockSize?: number 76 | /** 77 | * Use the `'light'` or `'dark'` color scheme instead of the system one. 78 | */ 79 | colorScheme?: ColorScheme 80 | /** 81 | * Font size for text in pixels. 82 | */ 83 | fontSize?: number 84 | /** 85 | * Localization strings for all calendar labels. 86 | * 87 | * `totalCount` supports the placeholders `{{count}}` and `{{year}}`. 88 | */ 89 | labels?: Labels 90 | /** 91 | * Maximum activity level (zero-indexed). 4 by default, 0 means "no activity". 92 | */ 93 | maxLevel?: number 94 | /** 95 | * Toggle to display the calendar loading state. The `data` property is 96 | * ignored if set. 97 | */ 98 | loading?: boolean 99 | /** 100 | * Ref to access the calendar DOM node. 101 | */ 102 | ref?: ForwardedRef 103 | /** 104 | * Render prop for calendar blocks (activities). For example, useful to 105 | * attach event handlers or to wrap the element with a link. Use 106 | * `React.cloneElement` to pass additional props to the element if necessary. 107 | */ 108 | renderBlock?: (block: BlockElement, activity: Activity) => ReactElement 109 | /** 110 | * Render prop for color legend blocks. Use `React.cloneElement` to pass 111 | * additional props to the element if necessary. 112 | */ 113 | renderColorLegend?: (block: BlockElement, level: number) => ReactElement 114 | /** 115 | * Toggle to hide the color legend below the calendar. 116 | */ 117 | showColorLegend?: boolean 118 | /** 119 | * Toggle to hide the month labels above the calendar. 120 | */ 121 | showMonthLabels?: boolean 122 | /** 123 | * Toggle to hide the total count below the calendar. 124 | */ 125 | showTotalCount?: boolean 126 | /** 127 | * Toggle to show weekday labels left to the calendar. 128 | * Alternatively, provide an array of ISO 8601 weekday names to display. 129 | * Example: `['mon', 'wed', 'fri']`. 130 | */ 131 | showWeekdayLabels?: boolean | Array 132 | /** 133 | * Style object to pass to the component container. 134 | */ 135 | style?: CSSProperties 136 | /** 137 | * Set the calendar colors for the light and dark color schemes. Provide 138 | * all colors per scheme explicitly (5 by default) or specify exactly two colors 139 | * (the lowest and highest intensity) to calculate a single-hue scale. The 140 | * number of colors is controlled by the `maxLevel` property. Colors can be 141 | * specified in any valid CSS format. 142 | * 143 | * At least one scheme's colors must be set. If undefined, the default 144 | * theme is used. By default, the calendar selects the current system color 145 | * scheme, but you can enforce a specific scheme with the `colorScheme` prop. 146 | * 147 | * Example: 148 | * 149 | * ```tsx 150 | * 157 | * ``` 158 | * 159 | */ 160 | theme?: ThemeInput 161 | /** 162 | * Tooltips to display when hovering over activity blocks or the color legend 163 | * below the calendar. See the story for details about tooltip configuration. 164 | */ 165 | tooltips?: { 166 | activity?: TooltipConfig & { 167 | text: (activity: Activity) => string 168 | } 169 | colorLegend?: TooltipConfig & { 170 | text: (level: number) => string 171 | } 172 | } 173 | /** 174 | * Index of day to be used as start of week. 0 represents Sunday. 175 | */ 176 | weekStart?: DayIndex 177 | } 178 | 179 | export const ActivityCalendar = forwardRef( 180 | ( 181 | { 182 | data: activities, 183 | blockMargin = 4, 184 | blockRadius = 2, 185 | blockSize = 12, 186 | colorScheme: colorSchemeProp = undefined, 187 | fontSize = 14, 188 | labels: labelsProp = undefined, 189 | loading = false, 190 | maxLevel = 4, 191 | renderBlock = undefined, 192 | renderColorLegend = undefined, 193 | showColorLegend = true, 194 | showMonthLabels = true, 195 | showTotalCount = true, 196 | showWeekdayLabels = false, 197 | style: styleProp = {}, 198 | theme: themeProp = undefined, 199 | tooltips = {}, 200 | weekStart = 0, // Sunday 201 | }, 202 | ref, 203 | ) => { 204 | const [isClient, setIsClient] = useState(false) 205 | useEffect(() => { 206 | setIsClient(true) 207 | }, []) 208 | 209 | maxLevel = Math.max(1, maxLevel) 210 | 211 | const theme = createTheme(themeProp, maxLevel + 1) 212 | const systemColorScheme = useColorScheme() 213 | const colorScheme = colorSchemeProp ?? systemColorScheme 214 | const colorScale = theme[colorScheme] 215 | 216 | useLoadingAnimation(colorScale[0] as string, colorScheme) 217 | const useAnimation = !usePrefersReducedMotion() 218 | 219 | if (loading) { 220 | activities = generateEmptyData() 221 | } 222 | 223 | validateActivities(activities, maxLevel) 224 | 225 | const firstActivity = activities[0] as Activity 226 | const year = getYear(parseISO(firstActivity.date)) 227 | const weeks = groupByWeeks(activities, weekStart) 228 | 229 | const labels = Object.assign({}, DEFAULT_LABELS, labelsProp) 230 | const labelHeight = showMonthLabels ? fontSize + LABEL_MARGIN : 0 231 | 232 | const weekdayLabels = initWeekdayLabels(showWeekdayLabels, weekStart) 233 | 234 | // Must be calculated on the client or SSR hydration errors will occur 235 | // because server and client HTML would not match. 236 | const weekdayLabelOffset = 237 | isClient && weekdayLabels.shouldShow 238 | ? maxWeekdayLabelWidth(labels.weekdays, weekdayLabels, fontSize) + LABEL_MARGIN 239 | : undefined 240 | 241 | function getDimensions() { 242 | return { 243 | width: weeks.length * (blockSize + blockMargin) - blockMargin, 244 | height: labelHeight + (blockSize + blockMargin) * 7 - blockMargin, 245 | } 246 | } 247 | 248 | function renderCalendar() { 249 | return weeks 250 | .map((week, weekIndex) => 251 | week.map((activity, dayIndex) => { 252 | if (!activity) { 253 | return null 254 | } 255 | 256 | const loadingAnimation = 257 | loading && useAnimation 258 | ? { 259 | animation: `${loadingAnimationName} 1.75s ease-in-out infinite`, 260 | animationDelay: `${weekIndex * 20 + dayIndex * 20}ms`, 261 | } 262 | : undefined 263 | 264 | const block = ( 265 | 280 | ) 281 | 282 | const renderedBlock = renderBlock ? renderBlock(block, activity) : block 283 | 284 | return ( 285 | 286 | {tooltips.activity ? ( 287 | 288 | 297 | {renderedBlock} 298 | 299 | 300 | ) : ( 301 | renderedBlock 302 | )} 303 | 304 | ) 305 | }), 306 | ) 307 | .map((week, x) => ( 308 | 309 | {week} 310 | 311 | )) 312 | } 313 | 314 | function renderFooter() { 315 | if (!showTotalCount && !showColorLegend) { 316 | return null 317 | } 318 | 319 | const totalCount = activities.reduce((sum, activity) => sum + activity.count, 0) 320 | 321 | return ( 322 |
326 | {/* Placeholder */} 327 | {loading &&
 
} 328 | 329 | {!loading && showTotalCount && ( 330 |
331 | {labels.totalCount 332 | ? labels.totalCount 333 | .replace('{{count}}', String(totalCount)) 334 | .replace('{{year}}', String(year)) 335 | : `${totalCount} activities in ${year}`} 336 |
337 | )} 338 | 339 | {!loading && showColorLegend && ( 340 |
341 | {labels.legend.less} 342 | {range(maxLevel + 1).map(level => { 343 | const colorLegend = ( 344 | 345 | 353 | 354 | ) 355 | 356 | const renderedColorLegend = renderColorLegend 357 | ? renderColorLegend(colorLegend, level) 358 | : colorLegend 359 | 360 | return ( 361 | 362 | {tooltips.colorLegend ? ( 363 | 364 | 373 | {renderedColorLegend} 374 | 375 | 376 | ) : ( 377 | renderedColorLegend 378 | )} 379 | 380 | ) 381 | })} 382 | {labels.legend.more} 383 |
384 | )} 385 |
386 | ) 387 | } 388 | 389 | function renderWeekdayLabels() { 390 | if (!weekdayLabels.shouldShow) { 391 | return null 392 | } 393 | 394 | return ( 395 | 396 | {range(7).map(index => { 397 | const dayIndex = ((index + weekStart) % 7) as DayIndex 398 | 399 | if (!weekdayLabels.byDayIndex(dayIndex)) { 400 | return null 401 | } 402 | 403 | return ( 404 | 412 | {labels.weekdays[dayIndex]} 413 | 414 | ) 415 | })} 416 | 417 | ) 418 | } 419 | 420 | function renderMonthLabels() { 421 | if (!showMonthLabels) { 422 | return null 423 | } 424 | 425 | return ( 426 | 427 | {getMonthLabels(weeks, labels.months).map(({ label, weekIndex }) => ( 428 | 435 | {label} 436 | 437 | ))} 438 | 439 | ) 440 | } 441 | 442 | const { width, height } = getDimensions() 443 | 444 | return ( 445 |
450 |
451 | 458 | {!loading && renderWeekdayLabels()} 459 | {!loading && renderMonthLabels()} 460 | {renderCalendar()} 461 | 462 |
463 | {renderFooter()} 464 |
465 | ) 466 | }, 467 | ) 468 | 469 | ActivityCalendar.displayName = 'ActivityCalendar' 470 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cloneElement, useRef, useState, type ReactElement, type Ref } from 'react' 4 | import { 5 | arrow, 6 | autoUpdate, 7 | flip, 8 | FloatingArrow, 9 | FloatingPortal, 10 | offset, 11 | shift, 12 | useDismiss, 13 | useFloating, 14 | useHover, 15 | useInteractions, 16 | useRole, 17 | useTransitionStyles, 18 | type OffsetOptions, 19 | type Placement, 20 | type UseHoverProps, 21 | type UseTransitionStylesProps, 22 | } from '@floating-ui/react' 23 | import { getClassName } from '../lib/calendar' 24 | import type { ColorScheme } from '../types' 25 | 26 | export type TooltipConfig = { 27 | placement?: Placement 28 | offset?: OffsetOptions 29 | transitionStyles?: UseTransitionStylesProps 30 | hoverRestMs?: UseHoverProps['restMs'] 31 | withArrow?: boolean 32 | } 33 | 34 | export type TooltipProps = TooltipConfig & { 35 | children: ReactElement<{ ref: Ref }> 36 | text: string 37 | colorScheme: ColorScheme 38 | } 39 | 40 | export function Tooltip({ 41 | children, 42 | text, 43 | colorScheme, 44 | placement, 45 | offset: offsetProp = 4, 46 | transitionStyles: transitionStylesProp, 47 | hoverRestMs = 150, 48 | withArrow = false, 49 | }: TooltipProps) { 50 | const [isOpen, setIsOpen] = useState(false) 51 | const arrowRef = useRef(null) 52 | 53 | const { context, refs, floatingStyles } = useFloating({ 54 | open: isOpen, 55 | onOpenChange: setIsOpen, 56 | placement, 57 | middleware: [ 58 | flip(), 59 | offset(offsetProp), 60 | shift({ padding: 8 }), 61 | withArrow ? arrow({ element: arrowRef }) : null, // eslint-disable-line react-hooks/refs 62 | ], 63 | whileElementsMounted: autoUpdate, 64 | }) 65 | 66 | const hover = useHover(context, { restMs: hoverRestMs }) 67 | const dismiss = useDismiss(context) 68 | const role = useRole(context, { role: 'tooltip' }) 69 | 70 | const { getReferenceProps, getFloatingProps } = useInteractions([hover, dismiss, role]) 71 | 72 | const { isMounted, styles: transitionStyles } = useTransitionStyles(context, transitionStylesProp) 73 | 74 | return ( 75 | <> 76 | {cloneElement(children, { ref: refs.setReference, ...getReferenceProps() })} 77 | { 78 | 79 | {isMounted && ( 80 |
87 | {text} 88 | {withArrow && ( 89 | 94 | )} 95 |
96 | )} 97 |
98 | } 99 | 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const NAMESPACE = 'react-activity-calendar' 2 | export const LABEL_MARGIN = 8 // px 3 | 4 | export const DEFAULT_MONTH_LABELS = [ 5 | 'Jan', 6 | 'Feb', 7 | 'Mar', 8 | 'Apr', 9 | 'May', 10 | 'Jun', 11 | 'Jul', 12 | 'Aug', 13 | 'Sep', 14 | 'Oct', 15 | 'Nov', 16 | 'Dec', 17 | ] 18 | 19 | export const DEFAULT_LABELS = { 20 | months: DEFAULT_MONTH_LABELS, 21 | weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 22 | totalCount: '{{count}} activities in {{year}}', 23 | legend: { 24 | less: 'Less', 25 | more: 'More', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/docs/ActivityCalendar.upgrading.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks' 2 | // noinspection ES6UnusedImports 3 | import LinkTo from '@storybook/addon-links/react' 4 | import { Source } from './Source.tsx' 5 | 6 | 7 | 8 | # Upgrading to v3 9 | 10 | Version 3 of React Activity Calendar introduces several breaking changes and a introduces a new 11 | approach to tooltips. Follow the guide below to upgrade your project from v2 to v3. 12 | 13 | ## Breaking changes 14 | 15 | - React Activity Calendar is now a 16 | [pure ESM package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). 17 | - The default export has been **removed**. Use the named export instead: 18 | 19 | - The `eventHandlers` prop has been **removed**. Use the `renderBlock` prop with 20 | `React.cloneElement()` to attach 21 | event handlers. 22 | - The `totalCount` prop has been **removed**, overriding the total count is no longer supported. 23 | - The `hideColorLegend` prop has been **renamed** to `showColorLegend`. 24 | - The `hideMonthLabels` prop has been **renamed** to `showMonthLabels`. 25 | - The `hideTotalCount` prop has been **renamed** to `showTotalCount`. 26 | - The `` component has been **removed**.
Render the calendar with empty data in 27 | its loading state instead: 28 | 29 | 30 | ## Tooltips 31 | 32 | Tooltips no longer depend on external libraries and are now integrated directly into this package. 33 | Thanks to code-splitting, tooltips only affect your bundle size when you use them. They are 34 | implemented using the [Floating UI](https://floating-ui.com/) library as a “headless” component, 35 | meaning they come without predefined styles. This gives you full control over the appearance: 36 | 37 | - Import the default styles provided by this package, **or** 38 | - Add your own custom CSS. 39 | 40 | See the tooltips page for details 41 | and examples. 42 | -------------------------------------------------------------------------------- /src/docs/Source.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Highlight, themes as prismThemes } from 'prism-react-renderer' 3 | import { themes } from 'storybook/theming' 4 | 5 | export const Source = ({ 6 | code, 7 | isDarkMode, 8 | language = 'tsx', 9 | }: { 10 | code: string 11 | isDarkMode: boolean 12 | language?: string 13 | }) => { 14 | const [copied, setCopied] = useState(false) 15 | const theme = isDarkMode ? themes.dark : themes.light 16 | 17 | return ( 18 |
19 | 24 | {({ style, tokens, getLineProps, getTokenProps }) => ( 25 |
36 |             
63 |             {tokens.map((line, i) => (
64 |               
65 | {line.map((token, key) => ( 66 | 67 | ))} 68 |
69 | ))} 70 |
71 | )} 72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import type { ColorScheme } from '../types' 3 | 4 | export function useColorScheme() { 5 | const [colorScheme, setColorScheme] = useState('light') 6 | 7 | const onChange = (event: MediaQueryListEvent) => { 8 | setColorScheme(event.matches ? 'dark' : 'light') 9 | } 10 | 11 | useEffect(() => { 12 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 13 | 14 | // eslint-disable-next-line react-hooks/set-state-in-effect 15 | setColorScheme(mediaQuery.matches ? 'dark' : 'light') 16 | 17 | mediaQuery.addEventListener('change', onChange) 18 | 19 | return () => { 20 | mediaQuery.removeEventListener('change', onChange) 21 | } 22 | }, []) 23 | 24 | return colorScheme 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useLoadingAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { NAMESPACE } from '../constants' 3 | import type { ColorScheme } from '../types' 4 | 5 | export const loadingAnimationName = `${NAMESPACE}--loading-animation` 6 | 7 | export function useLoadingAnimation(zeroColor: string, colorScheme: ColorScheme) { 8 | useEffect(() => { 9 | const colorLoading = `oklab(from ${zeroColor} l a b)` 10 | const colorActive = 11 | colorScheme === 'light' 12 | ? `oklab(from ${zeroColor} calc(l * 0.96) a b)` 13 | : `oklab(from ${zeroColor} calc(l * 1.08) a b)` 14 | 15 | const style = document.createElement('style') 16 | style.innerHTML = ` 17 | @keyframes ${loadingAnimationName} { 18 | 0% { 19 | fill: ${colorLoading}; 20 | } 21 | 50% { 22 | fill: ${colorActive}; 23 | } 24 | 100% { 25 | fill: ${colorLoading}; 26 | } 27 | } 28 | ` 29 | document.head.appendChild(style) 30 | 31 | return () => { 32 | document.head.removeChild(style) 33 | } 34 | }, [zeroColor, colorScheme]) 35 | } 36 | -------------------------------------------------------------------------------- /src/hooks/usePrefersReducedMotion.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const query = '(prefers-reduced-motion: reduce)' 4 | 5 | export function usePrefersReducedMotion() { 6 | const [prefersReducedMotion, setPrefersReducedMotion] = useState(true) 7 | 8 | useEffect(() => { 9 | const mediaQuery = window.matchMedia(query) 10 | 11 | // eslint-disable-next-line react-hooks/set-state-in-effect 12 | setPrefersReducedMotion(mediaQuery.matches) 13 | 14 | const onChange = (event: MediaQueryListEvent) => { 15 | setPrefersReducedMotion(event.matches) 16 | } 17 | 18 | mediaQuery.addEventListener('change', onChange) 19 | 20 | return () => { 21 | mediaQuery.removeEventListener('change', onChange) 22 | } 23 | }, []) 24 | 25 | return prefersReducedMotion 26 | } 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityCalendar } from './components/ActivityCalendar' 2 | 3 | export { ActivityCalendar } 4 | -------------------------------------------------------------------------------- /src/lib/calendar.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals' 2 | import type { Activity } from '../types' 3 | import { validateActivities } from './calendar' 4 | 5 | describe('validateActivities', () => { 6 | it.each([ 7 | ['empty array', [], 4], 8 | ['invalid date', [{ date: 'invalid', count: 0, level: 0 }], 4], 9 | ['non-existing date', [{ date: '2023-02-29', count: 0, level: 0 }], 4], 10 | ['negative count', [{ date: '2024-01-01', count: -1, level: 0 }], 4], 11 | ['negative level', [{ date: '2024-01-01', count: 0, level: -1 }], 4], 12 | ['invalid level', [{ date: '2024-01-01', count: 0, level: 5 }], 4], 13 | ] as Array<[string, Array, number]>)( 14 | 'should throw error for invalid input: %s', 15 | (_, activities, maxLevel) => { 16 | expect(() => { 17 | validateActivities(activities, maxLevel) 18 | }).toThrow() 19 | }, 20 | ) 21 | 22 | it.each([ 23 | [[{ date: '2024-01-01', count: 0, level: 0 }], 4], 24 | [[{ date: '2024-02-29', count: 4, level: 4 }], 4], 25 | [[{ date: '2024-12-31', count: 10, level: 10 }], 10], 26 | ] as Array<[Array, number]>)('should accept valid input', (activities, maxLevel) => { 27 | expect(() => { 28 | validateActivities(activities, maxLevel) 29 | }).not.toThrow() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/lib/calendar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | differenceInCalendarDays, 3 | eachDayOfInterval, 4 | endOfYear, 5 | formatISO, 6 | getDay, 7 | isValid, 8 | nextDay, 9 | parseISO, 10 | startOfYear, 11 | subWeeks, 12 | } from 'date-fns' 13 | import { NAMESPACE } from '../constants' 14 | import type { Activity, DayIndex, Week } from '../types' 15 | 16 | export function validateActivities(activities: Array, maxLevel: number) { 17 | if (activities.length === 0) { 18 | throw new Error('Activity data must not be empty.') 19 | } 20 | 21 | for (const { date, level, count } of activities) { 22 | if (!isValid(parseISO(date))) { 23 | throw new Error(`Activity date '${date}' is not a valid ISO 8601 date string.`) 24 | } 25 | 26 | if (count < 0) { 27 | throw new RangeError(`Activity count must not be negative, found ${count}.`) 28 | } 29 | 30 | if (level < 0 || level > maxLevel) { 31 | throw new RangeError( 32 | `Activity level ${level} for ${date} is out of range. It must be between 0 and ${maxLevel}.`, 33 | ) 34 | } 35 | } 36 | } 37 | 38 | export function groupByWeeks( 39 | activities: Array, 40 | weekStart: DayIndex = 0, // 0 = Sunday 41 | ): Array { 42 | const normalizedActivities = fillHoles(activities) 43 | 44 | // Determine the first date of the calendar. If the first date is not the 45 | // passed weekday, the respective weekday one week earlier is used. 46 | const firstActivity = normalizedActivities[0] as Activity 47 | const firstDate = parseISO(firstActivity.date) 48 | const firstCalendarDate = 49 | getDay(firstDate) === weekStart ? firstDate : subWeeks(nextDay(firstDate, weekStart), 1) 50 | 51 | // To correctly group activities by week, it is necessary to left-pad the list 52 | // because the first date might not be the set start weekday. 53 | const paddedActivities = [ 54 | ...(Array(differenceInCalendarDays(firstDate, firstCalendarDate)).fill( 55 | undefined, 56 | ) as Array), 57 | ...normalizedActivities, 58 | ] 59 | 60 | const numberOfWeeks = Math.ceil(paddedActivities.length / 7) 61 | 62 | // Finally, group activities by week 63 | return range(numberOfWeeks).map(weekIndex => 64 | paddedActivities.slice(weekIndex * 7, weekIndex * 7 + 7), 65 | ) 66 | } 67 | 68 | /** 69 | * The calendar expects a continuous sequence of days, 70 | * so fill gaps with empty activity data. 71 | */ 72 | function fillHoles(activities: Array): Array { 73 | const calendar = new Map(activities.map(a => [a.date, a])) 74 | const firstActivity = activities[0] as Activity 75 | const lastActivity = activities[activities.length - 1] as Activity 76 | 77 | return eachDayOfInterval({ 78 | start: parseISO(firstActivity.date), 79 | end: parseISO(lastActivity.date), 80 | }).map(day => { 81 | const date = formatISO(day, { representation: 'date' }) 82 | 83 | if (calendar.has(date)) { 84 | return calendar.get(date) as Activity 85 | } 86 | 87 | return { 88 | date, 89 | count: 0, 90 | level: 0, 91 | } 92 | }) 93 | } 94 | 95 | /** 96 | * Following the BEM (block, element, modifier) naming convention 97 | * https://getbem.com/naming/ 98 | */ 99 | export function getClassName(element: string) { 100 | return `${NAMESPACE}__${element}` 101 | } 102 | 103 | export function range(n: number) { 104 | return [...Array(n).keys()] 105 | } 106 | 107 | export function generateEmptyData(): Array { 108 | const year = new Date().getFullYear() 109 | const days = eachDayOfInterval({ 110 | start: new Date(year, 0, 1), 111 | end: new Date(year, 11, 31), 112 | }) 113 | 114 | return days.map(date => ({ 115 | date: formatISO(date, { representation: 'date' }), 116 | count: 0, 117 | level: 0, 118 | })) 119 | } 120 | 121 | export function generateTestData(args: { 122 | interval?: { start: Date; end: Date } 123 | maxLevel?: number 124 | }): Array { 125 | const maxCount = 20 126 | const maxLevel = args.maxLevel ? Math.max(1, args.maxLevel) : 4 127 | const now = new Date() 128 | 129 | const days = eachDayOfInterval( 130 | args.interval ?? { 131 | start: startOfYear(now), 132 | end: endOfYear(now), 133 | }, 134 | ) 135 | 136 | const noise = whiteNoise(days.length, maxLevel, v => 0.9 * Math.pow(v, 2)) 137 | 138 | return days.map((date, i) => { 139 | const level = noise[i] as number 140 | 141 | return { 142 | date: formatISO(date, { representation: 'date' }), 143 | count: Math.round(level * (maxCount / maxLevel)), 144 | level, 145 | } 146 | }) 147 | } 148 | 149 | // Deterministically generates n white noise values from 0 to max (inclusive). 150 | function whiteNoise( 151 | n: number, 152 | max: number, 153 | transformFn: (v: number) => number = v => v, 154 | ): Array { 155 | const seed = 54321 156 | const random = mulberry32(seed) 157 | 158 | return Array.from({ length: n }, () => { 159 | const v = transformFn(random()) 160 | return Math.floor(v * (max + 1)) 161 | }) 162 | } 163 | 164 | // Mulberry32 pseudorandom number generator 165 | function mulberry32(seed: number) { 166 | let state = seed >>> 0 // ensure unsigned 32-bit integer 167 | 168 | return () => { 169 | state += 0x6d2b79f5 170 | let r = Math.imul(state ^ (state >>> 15), 1 | state) 171 | r ^= r + Math.imul(r ^ (r >>> 7), 61 | r) 172 | 173 | return ((r ^ (r >>> 14)) >>> 0) / 4294967296 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/lib/label.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@jest/globals' 2 | import type { DayIndex, Week } from '../types' 3 | import { generateTestData, groupByWeeks } from './calendar' 4 | import { getMonthLabels, initWeekdayLabels } from './label' 5 | 6 | describe('getMonthLabels', () => { 7 | it('returns empty list for empty input', () => { 8 | expect(getMonthLabels([])).toStrictEqual([]) 9 | }) 10 | 11 | it('throws if a week has no activity defined', () => { 12 | const weeks = [Array(7).fill(undefined)] as Array 13 | 14 | expect(() => { 15 | expect(getMonthLabels(weeks)).toStrictEqual([]) 16 | }).toThrow() 17 | }) 18 | 19 | it('returns correct month labels', () => { 20 | const weeks = groupByWeeks( 21 | generateTestData({ 22 | interval: { 23 | start: new Date(2023, 2, 12), 24 | end: new Date(2023, 5, 1), 25 | }, 26 | }), 27 | ) 28 | 29 | expect(getMonthLabels(weeks)).toStrictEqual([ 30 | { label: 'Mar', weekIndex: 0 }, 31 | { label: 'Apr', weekIndex: 3 }, 32 | { label: 'May', weekIndex: 8 }, 33 | ]) 34 | }) 35 | 36 | it('skips label for first month if it does not contain at least three weeks of data', () => { 37 | const weeks = groupByWeeks( 38 | generateTestData({ 39 | interval: { 40 | start: new Date(2023, 9, 22), 41 | end: new Date(2023, 11, 31), 42 | }, 43 | }), 44 | ) 45 | 46 | expect(getMonthLabels(weeks)).toStrictEqual([ 47 | { label: 'Nov', weekIndex: 2 }, 48 | { label: 'Dec', weekIndex: 6 }, 49 | ]) 50 | }) 51 | 52 | it('skips label for last month if it does not contain at least three weeks of data', () => { 53 | const weeks = groupByWeeks( 54 | generateTestData({ 55 | interval: { 56 | start: new Date(2023, 3, 1), 57 | end: new Date(2023, 4, 20), 58 | }, 59 | }), 60 | ) 61 | 62 | expect(getMonthLabels(weeks)).toStrictEqual([{ label: 'Apr', weekIndex: 0 }]) 63 | }) 64 | 65 | it('skips first and last label if both months do not contain at least three weeks of data', () => { 66 | const weeks = groupByWeeks( 67 | generateTestData({ 68 | interval: { 69 | start: new Date(2023, 1, 22), 70 | end: new Date(2023, 4, 10), 71 | }, 72 | }), 73 | ) 74 | 75 | expect(getMonthLabels(weeks)).toStrictEqual([ 76 | { label: 'Mar', weekIndex: 2 }, 77 | { label: 'Apr', weekIndex: 6 }, 78 | ]) 79 | }) 80 | }) 81 | 82 | describe('initWeekdayLabels', () => { 83 | const days: Array = [0, 1, 2, 3, 4, 5, 6] 84 | 85 | it.each([[undefined], [false]])('should return false for `%p` as input', input => { 86 | for (const weekStart of days) { 87 | const actual = initWeekdayLabels(input, weekStart) 88 | 89 | expect(actual.shouldShow).toBe(false) 90 | for (const dayIndex of days) { 91 | expect(actual.byDayIndex(dayIndex)).toBe(false) 92 | } 93 | } 94 | }) 95 | 96 | it.each([ 97 | [0, [false, true, false, true, false, true, false]], 98 | [1, [false, false, true, false, true, false, true]], 99 | [2, [true, false, false, true, false, true, false]], 100 | [3, [false, true, false, false, true, false, true]], 101 | [4, [true, false, true, false, false, true, false]], 102 | [5, [false, true, false, true, false, false, true]], 103 | [6, [true, false, true, false, true, false, false]], 104 | ])( 105 | 'should return true for every second weekday for `true` as input with %d as week start', 106 | (weekStart, expected) => { 107 | const actual = initWeekdayLabels(true, weekStart as DayIndex) 108 | 109 | expect(actual.shouldShow).toBe(true) 110 | for (const weekStart of days) { 111 | expect(actual.byDayIndex(weekStart)).toBe(expected[weekStart]) 112 | } 113 | }, 114 | ) 115 | 116 | it('should return false for no days as input', () => { 117 | for (const weekStart of days) { 118 | const actual = initWeekdayLabels([], weekStart) 119 | 120 | expect(actual.shouldShow).toBe(false) 121 | for (const dayIndex of days) { 122 | expect(actual.byDayIndex(dayIndex)).toBe(false) 123 | } 124 | } 125 | }) 126 | 127 | it('should return true for given days', () => { 128 | for (const weekStart of days) { 129 | const actual = initWeekdayLabels(['mon', 'wed', 'sat'], weekStart) 130 | 131 | expect(actual.shouldShow).toBe(true) 132 | expect(actual.byDayIndex(0)).toBe(false) 133 | expect(actual.byDayIndex(1)).toBe(true) 134 | expect(actual.byDayIndex(2)).toBe(false) 135 | expect(actual.byDayIndex(3)).toBe(true) 136 | expect(actual.byDayIndex(4)).toBe(false) 137 | expect(actual.byDayIndex(5)).toBe(false) 138 | expect(actual.byDayIndex(6)).toBe(true) 139 | } 140 | }) 141 | 142 | it('should return true all days as input', () => { 143 | for (const weekStart of days) { 144 | const actual = initWeekdayLabels(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], weekStart) 145 | 146 | expect(actual.shouldShow).toBe(true) 147 | for (const dayIndex of days) { 148 | expect(actual.byDayIndex(dayIndex)).toBe(true) 149 | } 150 | } 151 | }) 152 | 153 | it('should handle wrong capitalization correctly', () => { 154 | // @ts-expect-error we want to test incorrect input 155 | const actual = initWeekdayLabels(['SUN'], 0) 156 | 157 | expect(actual.shouldShow).toBe(true) 158 | for (const dayIndex of days) { 159 | expect(actual.byDayIndex(dayIndex)).toBe(dayIndex === 0) 160 | } 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /src/lib/label.ts: -------------------------------------------------------------------------------- 1 | import { getMonth, parseISO } from 'date-fns' 2 | import type { Props } from '../components/ActivityCalendar' 3 | import { DEFAULT_MONTH_LABELS } from '../constants' 4 | import type { DayIndex, DayName, Week, WeekdayLabels } from '../types' 5 | 6 | type MonthLabel = { 7 | weekIndex: number 8 | label: string 9 | } 10 | 11 | export function getMonthLabels( 12 | weeks: Array, 13 | monthNames: Array = DEFAULT_MONTH_LABELS, 14 | ): Array { 15 | return weeks 16 | .reduce>((labels, week, weekIndex) => { 17 | const firstActivity = week.find(activity => activity !== undefined) 18 | 19 | if (!firstActivity) { 20 | throw new Error(`Unexpected error: Week ${weekIndex + 1} is empty.`) 21 | } 22 | 23 | const month = monthNames[getMonth(parseISO(firstActivity.date))] 24 | 25 | if (!month) { 26 | const monthName = new Date(firstActivity.date).toLocaleString('en-US', { month: 'short' }) 27 | throw new Error(`Unexpected error: undefined month label for ${monthName}.`) 28 | } 29 | 30 | const prevLabel = labels[labels.length - 1] 31 | 32 | if (weekIndex === 0 || prevLabel?.label !== month) { 33 | return [...labels, { weekIndex, label: month }] 34 | } 35 | 36 | return labels 37 | }, []) 38 | .filter(({ weekIndex }, index, labels) => { 39 | // Labels should only be shown if there is "enough" space (data). 40 | // This is a naive implementation that does not take the block size, 41 | // font size, etc. into account. 42 | const minWeeks = 3 43 | 44 | // Skip the first month label if there is not enough space for the next one. 45 | if (index === 0) { 46 | return labels[1] && labels[1].weekIndex - weekIndex >= minWeeks 47 | } 48 | 49 | // Skip the last month label if there is not enough data in that month 50 | // to avoid overflowing the calendar on the right. 51 | if (index === labels.length - 1) { 52 | return weeks.slice(weekIndex).length >= minWeeks 53 | } 54 | 55 | return true 56 | }) 57 | } 58 | 59 | export function maxWeekdayLabelWidth( 60 | labels: Array, 61 | showWeekdayLabel: WeekdayLabels, 62 | fontSize: number, 63 | ): number { 64 | if (labels.length !== 7) { 65 | throw new Error('Exactly 7 labels, one for each weekday must be passed.') 66 | } 67 | 68 | return labels.reduce( 69 | (maxWidth, label, index) => 70 | showWeekdayLabel.byDayIndex(index as DayIndex) 71 | ? Math.max(maxWidth, Math.ceil(calcTextDimensions(label, fontSize).width)) 72 | : maxWidth, 73 | 0, 74 | ) 75 | } 76 | 77 | export function calcTextDimensions(text: string, fontSize: number) { 78 | if (typeof document === 'undefined' || typeof window === 'undefined') { 79 | return { width: 0, height: 0 } 80 | } 81 | 82 | if (fontSize < 1) { 83 | throw new RangeError('fontSize must be positive') 84 | } 85 | 86 | if (text.length === 0) { 87 | return { width: 0, height: 0 } 88 | } 89 | 90 | const namespace = 'http://www.w3.org/2000/svg' 91 | const svg = document.createElementNS(namespace, 'svg') 92 | 93 | svg.style.position = 'absolute' 94 | svg.style.visibility = 'hidden' 95 | svg.style.fontFamily = window.getComputedStyle(document.body).fontFamily 96 | svg.style.fontSize = `${fontSize}px` 97 | 98 | const textNode = document.createElementNS(namespace, 'text') 99 | textNode.textContent = text 100 | 101 | svg.appendChild(textNode) 102 | document.body.appendChild(svg) 103 | const boundingBox = textNode.getBBox() 104 | 105 | document.body.removeChild(svg) 106 | 107 | return { width: boundingBox.width, height: boundingBox.height } 108 | } 109 | 110 | export function initWeekdayLabels( 111 | input: Props['showWeekdayLabels'], 112 | weekStart: DayIndex, 113 | ): WeekdayLabels { 114 | if (!input) 115 | return { 116 | byDayIndex: () => false, 117 | shouldShow: false, 118 | } 119 | 120 | // Default: Show every second day of the week. 121 | if (input === true) { 122 | return { 123 | byDayIndex: index => { 124 | return ((7 + index - weekStart) % 7) % 2 !== 0 125 | }, 126 | shouldShow: true, 127 | } 128 | } 129 | 130 | const indexed: Array = [] 131 | for (const name of input) { 132 | const index = dayNameToIndex[name.toLowerCase() as DayName] 133 | indexed[index] = true 134 | } 135 | 136 | return { 137 | byDayIndex: index => indexed[index] ?? false, 138 | shouldShow: input.length > 0, 139 | } 140 | } 141 | 142 | const dayNameToIndex: Record = { 143 | sun: 0, 144 | mon: 1, 145 | tue: 2, 146 | wed: 3, 147 | thu: 4, 148 | fri: 5, 149 | sat: 6, 150 | } 151 | -------------------------------------------------------------------------------- /src/lib/theme.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, jest } from '@jest/globals' 2 | import type { Theme, ThemeInput } from '../types' 3 | import { createTheme } from './theme' 4 | 5 | describe('createTheme', () => { 6 | const defaultTheme = { 7 | light: [ 8 | 'hsl(0, 0%, 92%)', 9 | 'color-mix(in oklab, hsl(0, 0%, 26%) 25%, hsl(0, 0%, 92%))', 10 | 'color-mix(in oklab, hsl(0, 0%, 26%) 50%, hsl(0, 0%, 92%))', 11 | 'color-mix(in oklab, hsl(0, 0%, 26%) 75%, hsl(0, 0%, 92%))', 12 | 'hsl(0, 0%, 26%)', 13 | ], 14 | dark: [ 15 | 'hsl(0, 0%, 22%)', 16 | 'color-mix(in oklab, hsl(0, 0%, 92%) 25%, hsl(0, 0%, 22%))', 17 | 'color-mix(in oklab, hsl(0, 0%, 92%) 50%, hsl(0, 0%, 22%))', 18 | 'color-mix(in oklab, hsl(0, 0%, 92%) 75%, hsl(0, 0%, 22%))', 19 | 'hsl(0, 0%, 92%)', 20 | ], 21 | } 22 | 23 | const explicitTheme: Theme = { 24 | light: ['#f0f0f0', '#c4edde', '#7ac7c4', '#f73859', '#384259'], 25 | dark: ['hsl(0, 0%, 22%)', '#4D455D', '#7DB9B6', '#F5E9CF', '#E96479'], 26 | } 27 | 28 | const cssSpy = jest.fn() 29 | 30 | beforeEach(() => { 31 | // @ts-expect-error only this method needs to be mocked 32 | global.CSS = { 33 | supports: cssSpy.mockReturnValue(true), 34 | } 35 | }) 36 | 37 | it('returns the default theme if no input is passed', () => { 38 | expect(createTheme()).toStrictEqual(defaultTheme) 39 | }) 40 | 41 | it('throws an error if input is not an object', () => { 42 | // @ts-expect-error test invalid input 43 | expect(() => createTheme('invalid')).toThrow() 44 | }) 45 | 46 | it('throws an error if neither "light" or "dark" inputs are set', () => { 47 | // @ts-expect-error test invalid input 48 | expect(() => createTheme({})).toThrow() 49 | }) 50 | 51 | it('throws an error if too few colors are passed', () => { 52 | expect(() => 53 | createTheme( 54 | { 55 | light: ['blue'], 56 | }, 57 | 2, 58 | ), 59 | ).toThrow() 60 | }) 61 | 62 | it('throws an error if too many colors are passed', () => { 63 | expect(() => 64 | createTheme( 65 | { 66 | dark: Array(4).fill('green'), 67 | }, 68 | 3, 69 | ), 70 | ).toThrow() 71 | }) 72 | 73 | it('uses default dark color scale if undefined in input', () => { 74 | expect( 75 | createTheme({ 76 | light: explicitTheme.light, 77 | }), 78 | ).toStrictEqual({ 79 | light: explicitTheme.light, 80 | dark: defaultTheme.dark, 81 | }) 82 | }) 83 | 84 | it('uses default light color scale if undefined in input', () => { 85 | expect( 86 | createTheme({ 87 | dark: explicitTheme.dark, 88 | }), 89 | ).toStrictEqual({ 90 | light: defaultTheme.light, 91 | dark: explicitTheme.dark, 92 | }) 93 | }) 94 | 95 | it('throws if an invalid color is passed', () => { 96 | cssSpy.mockReturnValue(false) 97 | expect(() => 98 | createTheme({ 99 | dark: ['#333', '🙃'], 100 | }), 101 | ).toThrow() 102 | }) 103 | 104 | it('returns the same value for explicit inputs', () => { 105 | expect(createTheme(explicitTheme)).toStrictEqual(explicitTheme) 106 | }) 107 | 108 | it('calculates color scales for minimal input', () => { 109 | const input: ThemeInput = { 110 | light: ['hsl(0, 0%, 92%)', 'hsl(0, 0%, 26%)'], 111 | dark: ['hsl(0, 0%, 20%)', 'hsl(0, 0%, 92%)'], 112 | } 113 | 114 | const actual = createTheme(input) 115 | expect(actual.light).toHaveLength(5) 116 | expect(actual.dark).toHaveLength(5) 117 | }) 118 | 119 | it('calculates color scales with correct size', () => { 120 | const input: ThemeInput = { 121 | light: ['hsl(0, 0%, 92%)', 'hsl(0, 0%, 26%)'], 122 | dark: ['hsl(0, 0%, 20%)', 'hsl(0, 0%, 92%)'], 123 | } 124 | 125 | const actual = createTheme(input, 3) 126 | expect(actual.light).toHaveLength(3) 127 | expect(actual.dark).toHaveLength(3) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | import type { Color, ColorScale, Theme, ThemeInput } from '../types' 2 | import { range } from './calendar' 3 | 4 | export function createTheme(input?: ThemeInput, steps = 5): Theme { 5 | const defaultTheme = createDefaultTheme(steps) 6 | 7 | if (input) { 8 | validateThemeInput(input, steps) 9 | 10 | input.light = input.light ?? defaultTheme.light 11 | input.dark = input.dark ?? defaultTheme.dark 12 | 13 | return { 14 | light: isPair(input.light) ? calcColorScale(input.light, steps) : input.light, 15 | dark: isPair(input.dark) ? calcColorScale(input.dark, steps) : input.dark, 16 | } 17 | } 18 | 19 | return defaultTheme 20 | } 21 | 22 | function createDefaultTheme(steps: number): Theme { 23 | return { 24 | light: calcColorScale(['hsl(0, 0%, 92%)', 'hsl(0, 0%, 26%)'], steps), 25 | dark: calcColorScale(['hsl(0, 0%, 22%)', 'hsl(0, 0%, 92%)'], steps), 26 | } 27 | } 28 | 29 | function validateThemeInput(input: ThemeInput, steps: number) { 30 | const maxLevelHint = 'The number of colors is controlled by the "maxLevel" property.' 31 | 32 | if (typeof input !== 'object' || (input.light === undefined && input.dark === undefined)) { 33 | throw new Error( 34 | `The theme object must contain at least one of the fields "light" and "dark" with exactly 2 or ${steps} colors respectively. ${maxLevelHint}`, 35 | ) 36 | } 37 | 38 | if (input.light) { 39 | const { length } = input.light 40 | if (length !== 2 && length !== steps) { 41 | throw new Error( 42 | `theme.light must contain exactly 2 or ${steps} colors, ${length} passed. ${maxLevelHint}`, 43 | ) 44 | } 45 | 46 | for (const c of input.light) { 47 | if (typeof window !== 'undefined' && !CSS.supports('color', c)) { 48 | throw new Error(`Invalid color "${c}" passed. All CSS color formats are accepted.`) 49 | } 50 | } 51 | } 52 | 53 | if (input.dark) { 54 | const { length } = input.dark 55 | if (length !== 2 && length !== steps) { 56 | throw new Error( 57 | `theme.dark must contain exactly 2 or ${steps} colors, ${length} passed. ${maxLevelHint}`, 58 | ) 59 | } 60 | 61 | for (const c of input.dark) { 62 | if (typeof window !== 'undefined' && !CSS.supports('color', c)) { 63 | throw new Error(`Invalid color "${c}" passed. All CSS color formats are accepted.`) 64 | } 65 | } 66 | } 67 | } 68 | 69 | function calcColorScale([start, end]: [Color, Color], steps: number): ColorScale { 70 | return range(steps).map(i => { 71 | // In the loading animation the zero color is used. 72 | // However, Safari 16 crashes if a CSS color-mix expression like below is 73 | // combined with relative color syntax to calculate a hue variation for the 74 | // animation. Since the start and end colors do not need to be mixed, they 75 | // can be returned directly to work around this issue. 76 | switch (i) { 77 | case 0: 78 | return start 79 | case steps - 1: 80 | return end 81 | default: { 82 | const pos = (i / (steps - 1)) * 100 83 | return `color-mix(in oklab, ${end} ${parseFloat(pos.toFixed(2))}%, ${start})` 84 | } 85 | } 86 | }) 87 | } 88 | 89 | function isPair(val: Array): val is [T, T] { 90 | return val.length === 2 91 | } 92 | -------------------------------------------------------------------------------- /src/styles/styles.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react' 2 | import type { ColorScheme } from '../types' 3 | 4 | export const styles = { 5 | container: (fontSize: number) => 6 | ({ 7 | width: 'max-content', // Calendar should not grow 8 | maxWidth: '100%', // Do not remove - parent might be a flexbox 9 | display: 'flex', 10 | flexDirection: 'column', 11 | gap: '8px', 12 | fontSize: `${fontSize}px`, 13 | }) satisfies CSSProperties, 14 | scrollContainer: (fontSize: number) => 15 | ({ 16 | maxWidth: '100%', 17 | overflowX: 'auto', 18 | overflowY: 'hidden', 19 | paddingTop: Math.ceil(0.1 * fontSize), // SVG overflows in Firefox at y=0 20 | }) satisfies CSSProperties, 21 | calendar: { 22 | display: 'block', // SVGs are inline-block by default 23 | overflow: 'visible', // Weekday labels are rendered left of the container 24 | } satisfies CSSProperties, 25 | rect: (colorScheme: ColorScheme) => 26 | ({ 27 | stroke: colorScheme === 'light' ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.04)', 28 | }) satisfies CSSProperties, 29 | footer: { 30 | container: { 31 | display: 'flex', 32 | flexWrap: 'wrap', 33 | gap: '4px 16px', 34 | whiteSpace: 'nowrap', 35 | } satisfies CSSProperties, 36 | legend: { 37 | marginLeft: 'auto', 38 | display: 'flex', 39 | alignItems: 'center', 40 | gap: '3px', 41 | } satisfies CSSProperties, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/tooltips.css: -------------------------------------------------------------------------------- 1 | .react-activity-calendar__tooltip { 2 | width: max-content; 3 | max-width: calc(100vw - 20px); 4 | padding: 0.2em 0.5em; 5 | border-radius: 0.25em; 6 | background-color: hsl(0 0% 10%); 7 | color: hsl(0 0% 94%); 8 | font-size: 13px; 9 | 10 | /* See the `withArrow` setting. */ 11 | .react-activity-calendar__tooltip-arrow { 12 | fill: hsl(0 0% 10%); 13 | } 14 | 15 | /* Use this instead of a media query - the component theme can be set */ 16 | /* independent of the system color scheme. */ 17 | &[data-color-scheme='dark'] { 18 | background-color: hsl(0 0% 94%); 19 | color: hsl(0 0% 6%); 20 | 21 | .react-activity-calendar__tooltip-arrow { 22 | fill: hsl(0 0% 94%); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, JSXElementConstructor, ReactElement, SVGAttributes } from 'react' 2 | 3 | export type Activity = { 4 | date: string 5 | count: number 6 | level: number 7 | } 8 | 9 | export type Week = Array 10 | export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6 // 0 = Sunday, 1 = Monday etc. 11 | export type DayName = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' 12 | 13 | export type WeekdayLabels = { 14 | byDayIndex: (index: DayIndex) => boolean 15 | shouldShow: boolean 16 | } 17 | 18 | export type Labels = Partial<{ 19 | months: Array 20 | weekdays: Array 21 | totalCount: string 22 | legend: Partial<{ 23 | less: string 24 | more: string 25 | }> 26 | }> 27 | 28 | export type Color = string 29 | export type ColorScale = Array 30 | 31 | export type Theme = { 32 | light: ColorScale 33 | dark: ColorScale 34 | } 35 | 36 | // Require that at least one color scheme is passed. 37 | export type ThemeInput = 38 | | { 39 | light: ColorScale 40 | dark?: ColorScale 41 | } 42 | | { 43 | light?: ColorScale 44 | dark: ColorScale 45 | } 46 | 47 | export type ColorScheme = 'light' | 'dark' 48 | 49 | export type BlockElement = ReactElement< 50 | SVGAttributes & HTMLAttributes, 51 | JSXElementConstructor 52 | > 53 | -------------------------------------------------------------------------------- /tests/stories.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | test, 4 | type Page, 5 | type PageAssertionsToHaveScreenshotOptions, 6 | } from '@playwright/test' 7 | 8 | const options: PageAssertionsToHaveScreenshotOptions = { 9 | fullPage: true, 10 | } 11 | 12 | async function loadStory(page: Page, story: string) { 13 | await page.goto(`/iframe.html?id=react-activity-calendar--${story}`) 14 | await page.waitForSelector('.react-activity-calendar', { state: 'visible' }) 15 | } 16 | 17 | test('default', async ({ page }) => { 18 | await loadStory(page, 'default') 19 | await expect(page).toHaveScreenshot('default.png', options) 20 | }) 21 | 22 | test('loading', async ({ page }) => { 23 | await loadStory(page, 'loading') 24 | await expect(page).toHaveScreenshot('loading.png', options) 25 | }) 26 | 27 | test('activity-levels', async ({ page }) => { 28 | await loadStory(page, 'activity-levels') 29 | await expect(page).toHaveScreenshot('activity-levels.png', options) 30 | }) 31 | 32 | test('date-ranges', async ({ page }) => { 33 | await loadStory(page, 'date-ranges') 34 | await expect(page).toHaveScreenshot('date-ranges.png', options) 35 | }) 36 | 37 | test('color-themes', async ({ page }) => { 38 | await loadStory(page, 'color-themes') 39 | await expect(page).toHaveScreenshot('color-themes.png', options) 40 | }) 41 | 42 | test('explicit-themes', async ({ page }) => { 43 | await loadStory(page, 'explicit-themes') 44 | await expect(page).toHaveScreenshot('explicit-themes.png', options) 45 | }) 46 | 47 | test('customization', async ({ page }) => { 48 | await loadStory(page, 'customization') 49 | await expect(page).toHaveScreenshot('customization.png', options) 50 | }) 51 | 52 | test('event-handlers', async ({ page }) => { 53 | await loadStory(page, 'event-handlers') 54 | await expect(page).toHaveScreenshot('event-handlers.png', options) 55 | }) 56 | 57 | test('tooltips', async ({ page }) => { 58 | await loadStory(page, 'tooltips') 59 | await expect(page).toHaveScreenshot('tooltips.png', options) 60 | }) 61 | 62 | test('without-labels', async ({ page }) => { 63 | await loadStory(page, 'without-labels') 64 | await expect(page).toHaveScreenshot('without-labels.png', options) 65 | }) 66 | 67 | test('weekday-labels', async ({ page }) => { 68 | await loadStory(page, 'weekday-labels') 69 | await expect(page).toHaveScreenshot('weekday-labels.png', options) 70 | }) 71 | 72 | test('localized-labels', async ({ page }) => { 73 | await loadStory(page, 'localized-labels') 74 | await expect(page).toHaveScreenshot('localized-labels.png', options) 75 | }) 76 | 77 | test('monday-as-week-start', async ({ page }) => { 78 | await loadStory(page, 'monday-as-week-start') 79 | await expect(page).toHaveScreenshot('monday-as-week-start.png', options) 80 | }) 81 | 82 | test('narrow-screens', async ({ page }) => { 83 | await loadStory(page, 'narrow-screens') 84 | await expect(page).toHaveScreenshot('narrow-screens.png', options) 85 | }) 86 | 87 | test('container-ref', async ({ page }) => { 88 | await loadStory(page, 'container-ref') 89 | await expect(page).toHaveScreenshot('container-ref.png', options) 90 | }) 91 | -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/activity-levels-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/activity-levels-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/activity-levels-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/activity-levels-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/activity-levels-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/activity-levels-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/color-themes-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/color-themes-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/color-themes-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/color-themes-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/color-themes-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/color-themes-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/container-ref-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/container-ref-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/container-ref-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/container-ref-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/container-ref-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/container-ref-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/customization-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/customization-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/customization-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/customization-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/customization-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/customization-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/date-ranges-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/date-ranges-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/date-ranges-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/date-ranges-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/date-ranges-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/date-ranges-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/default-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/default-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/default-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/default-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/default-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/default-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/event-handlers-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/event-handlers-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/event-handlers-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/event-handlers-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/event-handlers-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/event-handlers-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/explicit-themes-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/explicit-themes-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/explicit-themes-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/explicit-themes-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/explicit-themes-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/explicit-themes-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/loading-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/loading-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/loading-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/loading-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/loading-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/loading-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/localized-labels-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/localized-labels-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/localized-labels-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/localized-labels-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/localized-labels-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/localized-labels-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/monday-as-week-start-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/monday-as-week-start-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/monday-as-week-start-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/monday-as-week-start-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/monday-as-week-start-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/monday-as-week-start-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/narrow-screens-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/narrow-screens-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/narrow-screens-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/narrow-screens-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/narrow-screens-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/narrow-screens-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/tooltips-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/tooltips-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/tooltips-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/tooltips-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/tooltips-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/tooltips-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/weekday-labels-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/weekday-labels-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/weekday-labels-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/weekday-labels-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/weekday-labels-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/weekday-labels-webkit-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/without-labels-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/without-labels-chromium-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/without-labels-firefox-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/without-labels-firefox-linux.png -------------------------------------------------------------------------------- /tests/stories.spec.ts-snapshots/without-labels-webkit-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grubersjoe/react-activity-calendar/da168478d27be7f39bada709ec54481551f8a532/tests/stories.spec.ts-snapshots/without-labels-webkit-linux.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "tests/**/*", ".storybook/**/*", "./*"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "isolatedModules": true, 7 | "jsx": "preserve", 8 | "lib": ["es2022", "dom", "dom.iterable"], 9 | "module": "preserve", 10 | "moduleDetection": "force", 11 | "noEmit": true, 12 | "noImplicitOverride": true, 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "es2022", 18 | "verbatimModuleSyntax": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }) 7 | --------------------------------------------------------------------------------