📄 index-CLOTiFb2.js
/home/palash/git/site/node_modules/react-activity-calendar/build/chunks/index-CLOTiFb2.js
Language: js • Lines: 687
'use client';
import { useState, useEffect, forwardRef, Fragment, Suspense, lazy } from 'react';
import { eachDayOfInterval, formatISO, isValid, parseISO, getDay, subWeeks, nextDay, differenceInCalendarDays, getMonth, getYear } from 'date-fns';
import { jsxs, jsx } from 'react/jsx-runtime';

const NAMESPACE = 'react-activity-calendar';
const LABEL_MARGIN = 8; // px

const DEFAULT_MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const DEFAULT_LABELS = {
  months: DEFAULT_MONTH_LABELS,
  weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  totalCount: '{{count}} activities in {{year}}',
  legend: {
    less: 'Less',
    more: 'More'
  }
};

const query$1 = '(prefers-color-scheme: dark)';
function useColorScheme() {
  const [colorScheme, setColorScheme] = useState(() => typeof window === 'undefined' ? 'light' : window.matchMedia(query$1).matches ? 'dark' : 'light');
  const onChange = event => {
    setColorScheme(event.matches ? 'dark' : 'light');
  };
  useEffect(() => {
    const mediaQuery = window.matchMedia(query$1);

    // eslint-disable-next-line react-hooks/set-state-in-effect
    setColorScheme(mediaQuery.matches ? 'dark' : 'light');
    mediaQuery.addEventListener('change', onChange);
    return () => {
      mediaQuery.removeEventListener('change', onChange);
    };
  }, []);
  return colorScheme;
}

const loadingAnimationName = `${NAMESPACE}--loading-animation`;
function useLoadingAnimation(zeroColor, colorScheme) {
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    const colorLoading = `oklab(from ${zeroColor} l a b)`;
    const colorActive = colorScheme === 'light' ? `oklab(from ${zeroColor} calc(l * 0.96) a b)` : `oklab(from ${zeroColor} calc(l * 1.08) a b)`;
    const style = document.createElement('style');
    style.innerHTML = `
      @keyframes ${loadingAnimationName} {
        0% {
          fill: ${colorLoading};
        }
        50% {
          fill: ${colorActive};
        }
        100% {
          fill: ${colorLoading};
        }
      }
    `;
    const handleLoad = () => {
      setLoaded(true);
    };
    document.head.appendChild(style);
    style.addEventListener('load', handleLoad);
    return () => {
      document.head.removeChild(style);
      style.removeEventListener('load', handleLoad);
      setLoaded(false);
    };
  }, [zeroColor, colorScheme]);
  return loaded;
}

const query = '(prefers-reduced-motion: reduce)';
function usePrefersReducedMotion() {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => typeof window === 'undefined' ? true : window.matchMedia(query).matches);
  useEffect(() => {
    const mediaQuery = window.matchMedia(query);

    // eslint-disable-next-line react-hooks/set-state-in-effect
    setPrefersReducedMotion(mediaQuery.matches);
    const onChange = event => {
      setPrefersReducedMotion(event.matches);
    };
    mediaQuery.addEventListener('change', onChange);
    return () => {
      mediaQuery.removeEventListener('change', onChange);
    };
  }, []);
  return prefersReducedMotion;
}

function validateActivities(activities, {
  minLevel,
  maxLevel
}) {
  if (activities.length === 0) {
    throw new Error('Activity data must not be empty.');
  }
  for (const {
    date,
    level
  } of activities) {
    if (!isValid(parseISO(date))) {
      throw new Error(`Activity date '${date}' is not a valid ISO 8601 date string.`);
    }
    if (level < minLevel || level > maxLevel) {
      throw new RangeError(`Activity level ${level} for ${date} is out of range. It must be between ${minLevel} and ${maxLevel}.`);
    }
  }
}
function groupByWeeks(activities, weekStart = 0 // 0 = Sunday
) {
  const normalizedActivities = fillHoles(activities);

  // Determine the first date of the calendar. If the first date is not the
  // passed weekday, the respective weekday one week earlier is used.
  const firstActivity = normalizedActivities[0];
  const firstDate = parseISO(firstActivity.date);
  const firstCalendarDate = getDay(firstDate) === weekStart ? firstDate : subWeeks(nextDay(firstDate, weekStart), 1);

  // To correctly group activities by week, it is necessary to left-pad the list
  // because the first date might not be the set start weekday.
  const paddedActivities = [...Array(differenceInCalendarDays(firstDate, firstCalendarDate)).fill(undefined), ...normalizedActivities];
  const numberOfWeeks = Math.ceil(paddedActivities.length / 7);

  // Finally, group activities by week
  return range(numberOfWeeks).map(weekIndex => paddedActivities.slice(weekIndex * 7, weekIndex * 7 + 7));
}

/**
 * The calendar expects a continuous sequence of days,
 * so fill gaps with empty activity data.
 */
function fillHoles(activities) {
  const calendar = new Map(activities.map(a => [a.date, a]));
  const firstActivity = activities[0];
  const lastActivity = activities[activities.length - 1];
  return eachDayOfInterval({
    start: parseISO(firstActivity.date),
    end: parseISO(lastActivity.date)
  }).map(day => {
    const date = formatISO(day, {
      representation: 'date'
    });
    if (calendar.has(date)) {
      return calendar.get(date);
    }
    return {
      date,
      count: 0,
      level: 0
    };
  });
}

/**
 * Following the BEM (block, element, modifier) naming convention
 * https://getbem.com/naming/
 */
function getClassName(element) {
  return `${NAMESPACE}__${element}`;
}
function generateEmptyData() {
  const year = new Date().getFullYear();
  const days = eachDayOfInterval({
    start: new Date(year, 0, 1),
    end: new Date(year, 11, 31)
  });
  return days.map(date => ({
    date: formatISO(date, {
      representation: 'date'
    }),
    count: 0,
    level: 0
  }));
}

/**
 * Returns the sequence of numbers in range.
 * @example
 * range(3) -> [0, 1, 2]
 * range(2, 5) -> [2, 3, 4]
 */
function range(fromArg, toArg) {
  const from = toArg === undefined ? 0 : fromArg;
  const to = toArg ?? fromArg;
  if (to <= from) {
    throw new RangeError('Invalid range: to must be greater than from');
  }
  return Array.from({
    length: to - from
  }, (_, i) => from + i);
}

function getMonthLabels(weeks, monthNames = DEFAULT_MONTH_LABELS) {
  return weeks.reduce((labels, week, weekIndex) => {
    const firstActivity = week.find(activity => activity !== undefined);
    if (!firstActivity) {
      throw new Error(`Unexpected error: Week ${weekIndex + 1} is empty.`);
    }
    const month = monthNames[getMonth(parseISO(firstActivity.date))];
    if (!month) {
      const monthName = new Date(firstActivity.date).toLocaleString('en-US', {
        month: 'short'
      });
      throw new Error(`Unexpected error: undefined month label for ${monthName}.`);
    }
    const prevLabel = labels[labels.length - 1];
    if (weekIndex === 0 || prevLabel?.label !== month) {
      return [...labels, {
        weekIndex,
        label: month
      }];
    }
    return labels;
  }, []).filter(({
    weekIndex
  }, index, labels) => {
    // Labels should only be shown if there is "enough" space (data).
    // This is a naive implementation that does not take the block size,
    // font size, etc. into account.
    const minWeeks = 3;

    // Skip the first month label if there is not enough space for the next one.
    if (index === 0) {
      return labels[1] && labels[1].weekIndex - weekIndex >= minWeeks;
    }

    // Skip the last month label if there is not enough data in that month
    // to avoid overflowing the calendar on the right.
    if (index === labels.length - 1) {
      return weeks.slice(weekIndex).length >= minWeeks;
    }
    return true;
  });
}
function maxWeekdayLabelWidth(labels, showWeekdayLabel, fontSize) {
  if (labels.length !== 7) {
    throw new Error('Exactly 7 labels, one for each weekday must be passed.');
  }
  return labels.reduce((maxWidth, label, index) => showWeekdayLabel.byDayIndex(index) ? Math.max(maxWidth, Math.ceil(calcTextDimensions(label, fontSize).width)) : maxWidth, 0);
}
function calcTextDimensions(text, fontSize) {
  if (typeof document === 'undefined' || typeof window === 'undefined') {
    return {
      width: 0,
      height: 0
    };
  }
  if (fontSize < 1) {
    throw new RangeError('fontSize must be positive');
  }
  if (text.length === 0) {
    return {
      width: 0,
      height: 0
    };
  }
  const namespace = 'http://www.w3.org/2000/svg';
  const svg = document.createElementNS(namespace, 'svg');
  svg.style.position = 'absolute';
  svg.style.visibility = 'hidden';
  svg.style.fontFamily = window.getComputedStyle(document.body).fontFamily;
  svg.style.fontSize = `${fontSize}px`;
  const textNode = document.createElementNS(namespace, 'text');
  textNode.textContent = text;
  svg.appendChild(textNode);
  document.body.appendChild(svg);
  const boundingBox = textNode.getBBox();
  document.body.removeChild(svg);
  return {
    width: boundingBox.width,
    height: boundingBox.height
  };
}
function initWeekdayLabels(input, weekStart) {
  if (!input) return {
    byDayIndex: () => false,
    shouldShow: false
  };

  // Default: Show every second day of the week.
  if (input === true) {
    return {
      byDayIndex: index => {
        return (7 + index - weekStart) % 7 % 2 !== 0;
      },
      shouldShow: true
    };
  }
  const indexed = [];
  for (const name of input) {
    const index = dayNameToIndex[name.toLowerCase()];
    indexed[index] = true;
  }
  return {
    byDayIndex: index => indexed[index] ?? false,
    shouldShow: input.length > 0
  };
}
const dayNameToIndex = {
  sun: 0,
  mon: 1,
  tue: 2,
  wed: 3,
  thu: 4,
  fri: 5,
  sat: 6
};

function createTheme(input, levels = {
  minLevel: 0,
  maxLevel: 4
}) {
  const defaultTheme = createDefaultTheme(levels);
  if (input) {
    const numberOfLevels = levels.maxLevel - levels.minLevel + 1;
    validateThemeInput(input, numberOfLevels);
    input.light = input.light ?? defaultTheme.light;
    input.dark = input.dark ?? defaultTheme.dark;
    return {
      light: isPair(input.light) ? calcColorScale([input.light[1], input.light[0], input.light[1]], levels) : isTriple(input.light) ? calcColorScale(input.light, levels) : input.light,
      dark: isPair(input.dark) ? calcColorScale([input.dark[1], input.dark[0], input.dark[1]], levels) : isTriple(input.dark) ? calcColorScale(input.dark, levels) : input.dark
    };
  }
  return defaultTheme;
}
function createDefaultTheme(levels) {
  return {
    light: calcColorScale(['hsl(0, 0%, 26%)', 'hsl(0, 0%, 92%)', 'hsl(0, 0%, 26%)'], levels),
    dark: calcColorScale(['hsl(0, 0%, 92%)', 'hsl(0, 0%, 22%)', 'hsl(0, 0%, 92%)'], levels)
  };
}
function validateThemeInput(input, numberOfLevels) {
  const levelsHint = 'The number of colors must match the number of activity levels controlled by the `minLevel` and `maxLevel` properties.';
  if (typeof input !== 'object' || input.light === undefined && input.dark === undefined) {
    throw new Error(`The theme object must contain at least one of the fields "light" and "dark" with exactly 2 or ${numberOfLevels} colors respectively. ${levelsHint}`);
  }
  if (input.light) {
    const {
      length
    } = input.light;
    if (length !== 2 && length !== 3 && length !== numberOfLevels) {
      throw new Error(`theme.light must contain exactly 2 or 3 or ${numberOfLevels} colors, ${length} passed. ${levelsHint}`);
    }
    for (const c of input.light) {
      if (typeof window !== 'undefined' && !CSS.supports('color', c)) {
        throw new Error(`Invalid color "${c}" passed. All CSS color formats are accepted.`);
      }
    }
  }
  if (input.dark) {
    const {
      length
    } = input.dark;
    if (length !== 2 && length !== 3 && length !== numberOfLevels) {
      throw new Error(`theme.dark must contain exactly 2 or 3 or ${numberOfLevels} colors, ${length} passed. ${levelsHint}`);
    }
    for (const c of input.dark) {
      if (typeof window !== 'undefined' && !CSS.supports('color', c)) {
        throw new Error(`Invalid color "${c}" passed. All CSS color formats are accepted.`);
      }
    }
  }
}
function calcColorScale([colorNeg, colorZero, colorPos], {
  minLevel,
  maxLevel
}) {
  return range(minLevel, maxLevel + 1).map(i => {
    if (i < 0) {
      if (i === minLevel) {
        return colorNeg;
      }
      const pos = (1 - i / minLevel) * 100;
      return `color-mix(in oklab, ${colorZero} ${parseFloat(pos.toFixed(2))}%, ${colorNeg})`;
    }
    if (i === 0) {
      return colorZero;
    }

    // i > 0
    if (i === maxLevel) {
      return colorPos;
    }
    const pos = i / maxLevel * 100;
    return `color-mix(in oklab, ${colorPos} ${parseFloat(pos.toFixed(2))}%, ${colorZero})`;
  });
}
function isPair(val) {
  return val.length === 2;
}
function isTriple(val) {
  return val.length === 3;
}

const styles = {
  container: fontSize => ({
    width: 'max-content',
    // Calendar should not grow
    maxWidth: '100%',
    // Do not remove - parent might be a flexbox
    display: 'flex',
    flexDirection: 'column',
    gap: '8px',
    fontSize: `${fontSize}px`
  }),
  scrollContainer: fontSize => ({
    maxWidth: '100%',
    overflowX: 'auto',
    overflowY: 'hidden',
    paddingTop: Math.ceil(0.1 * fontSize) // SVG <text> overflows in Firefox at y=0
  }),
  calendar: {
    display: 'block',
    // SVGs are inline-block by default
    overflow: 'visible' // Weekday labels are rendered left of the container
  },
  rect: colorScheme => ({
    stroke: colorScheme === 'light' ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.04)'
  }),
  footer: {
    container: {
      display: 'flex',
      flexWrap: 'wrap',
      gap: '4px 16px',
      whiteSpace: 'nowrap'
    },
    legend: {
      marginLeft: 'auto',
      display: 'flex',
      alignItems: 'center',
      gap: '3px'
    }
  }
};

const Tooltip = /*#__PURE__*/lazy(() => import('./Tooltip-B8gZJWmn.js').then(module => ({
  default: module.Tooltip
})));
const ActivityCalendar = /*#__PURE__*/forwardRef(({
  data: activities,
  blockMargin = 4,
  blockRadius = 2,
  blockSize = 12,
  className,
  colorScheme: colorSchemeProp,
  fontSize = 14,
  labels: labelsProp,
  loading = false,
  minLevel = 0,
  maxLevel = 4,
  renderBlock,
  renderColorLegend,
  showColorLegend = true,
  showMonthLabels = true,
  showTotalCount = true,
  showWeekdayLabels = false,
  style: styleProp = {},
  theme: themeProp,
  tooltips = {},
  weekStart = 0 // Sunday
}, ref) => {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => {
    setIsClient(true);
  }, []);
  if (minLevel >= maxLevel) {
    throw new RangeError(`Minimum activity level must be less than maximum level. Got ${minLevel} and ${maxLevel}.`);
  }
  const levels = {
    minLevel,
    maxLevel
  };
  const theme = createTheme(themeProp, levels);
  const systemColorScheme = useColorScheme();
  const colorScheme = colorSchemeProp ?? systemColorScheme;
  const colorScale = theme[colorScheme];
  const animationLoaded = useLoadingAnimation(colorScale[0], colorScheme);
  const useAnimation = !usePrefersReducedMotion();
  if (loading) {
    // Only show the loading state once the CSS animation has been injected
    // to avoid a flash of white with dark backgrounds.
    if (useAnimation && !animationLoaded) {
      return null;
    }
    activities = generateEmptyData();
  }
  validateActivities(activities, levels);
  const firstActivity = activities[0];
  const year = getYear(parseISO(firstActivity.date));
  const weeks = groupByWeeks(activities, weekStart);
  const labels = Object.assign({}, DEFAULT_LABELS, labelsProp);
  const labelHeight = showMonthLabels ? fontSize + LABEL_MARGIN : 0;
  const weekdayLabels = initWeekdayLabels(showWeekdayLabels, weekStart);

  // Must be calculated on the client, or SSR hydration errors will occur
  // because server and client HTML would not match.
  const weekdayLabelOffset = isClient && weekdayLabels.shouldShow ? maxWeekdayLabelWidth(labels.weekdays, weekdayLabels, fontSize) + LABEL_MARGIN : undefined;
  function getDimensions() {
    return {
      width: weeks.length * (blockSize + blockMargin) - blockMargin,
      height: labelHeight + (blockSize + blockMargin) * 7 - blockMargin
    };
  }
  function renderCalendar() {
    return weeks.map((week, weekIndex) => week.map((activity, dayIndex) => {
      if (!activity) {
        return null;
      }
      const loadingAnimation = loading && useAnimation ? {
        animation: `${loadingAnimationName} 1.75s ease-in-out infinite`,
        animationDelay: `${weekIndex * 20 + dayIndex * 20}ms`
      } : undefined;
      const block = /*#__PURE__*/jsx("rect", {
        x: 0,
        y: labelHeight + (blockSize + blockMargin) * dayIndex,
        width: blockSize,
        height: blockSize,
        rx: blockRadius,
        ry: blockRadius,
        fill: colorForLevel(activity.level),
        "data-date": activity.date,
        "data-level": activity.level,
        style: {
          ...styles.rect(colorScheme),
          ...loadingAnimation
        }
      });
      const renderedBlock = renderBlock ? renderBlock(block, activity) : block;
      return /*#__PURE__*/jsx(Fragment, {
        children: tooltips.activity ? /*#__PURE__*/jsx(Suspense, {
          fallback: renderedBlock,
          children: /*#__PURE__*/jsx(Tooltip, {
            text: tooltips.activity.text(activity),
            colorScheme: colorScheme,
            placement: tooltips.activity.placement ?? 'top',
            hoverRestMs: tooltips.activity.hoverRestMs,
            offset: tooltips.activity.offset,
            transitionStyles: tooltips.activity.transitionStyles,
            withArrow: tooltips.activity.withArrow,
            children: renderedBlock
          })
        }) : renderedBlock
      }, activity.date);
    })).map((week, x) => /*#__PURE__*/jsx("g", {
      transform: `translate(${(blockSize + blockMargin) * x}, 0)`,
      children: week
    }, x));
  }
  function renderFooter() {
    if (!showTotalCount && !showColorLegend) {
      return null;
    }
    const totalCount = activities.reduce((sum, activity) => sum + activity.count, 0);
    return /*#__PURE__*/jsxs("footer", {
      className: getClassName('footer'),
      style: {
        ...styles.footer.container,
        marginLeft: weekdayLabelOffset
      },
      children: [loading && /*#__PURE__*/jsx("div", {
        children: "\xA0"
      }), !loading && showTotalCount && /*#__PURE__*/jsx("div", {
        className: getClassName('count'),
        children: labels.totalCount ? labels.totalCount.replace('{{count}}', String(totalCount)).replace('{{year}}', String(year)) : `${totalCount} activities in ${year}`
      }), !loading && showColorLegend && /*#__PURE__*/jsxs("div", {
        className: getClassName('legend-colors'),
        style: styles.footer.legend,
        children: [labels.legend.less && /*#__PURE__*/jsx("span", {
          style: {
            marginRight: '0.4em'
          },
          children: labels.legend.less
        }), range(minLevel, maxLevel + 1).map(level => {
          const colorLegend = /*#__PURE__*/jsx("svg", {
            width: blockSize,
            height: blockSize,
            children: /*#__PURE__*/jsx("rect", {
              width: blockSize,
              height: blockSize,
              fill: colorForLevel(level),
              rx: blockRadius,
              ry: blockRadius,
              style: styles.rect(colorScheme)
            })
          }, level);
          const renderedColorLegend = renderColorLegend ? renderColorLegend(colorLegend, level) : colorLegend;
          return /*#__PURE__*/jsx(Fragment, {
            children: tooltips.colorLegend ? /*#__PURE__*/jsx(Suspense, {
              fallback: renderedColorLegend,
              children: /*#__PURE__*/jsx(Tooltip, {
                text: tooltips.colorLegend.text(level),
                colorScheme: colorScheme,
                placement: tooltips.colorLegend.placement ?? 'bottom',
                hoverRestMs: tooltips.colorLegend.hoverRestMs,
                offset: tooltips.colorLegend.offset,
                transitionStyles: tooltips.colorLegend.transitionStyles,
                withArrow: tooltips.colorLegend.withArrow,
                children: renderedColorLegend
              })
            }) : renderedColorLegend
          }, level);
        }), labels.legend.more && /*#__PURE__*/jsx("span", {
          style: {
            marginLeft: '0.4em'
          },
          children: labels.legend.more
        })]
      })]
    });
  }
  function renderWeekdayLabels() {
    if (!weekdayLabels.shouldShow) {
      return null;
    }
    return /*#__PURE__*/jsx("g", {
      className: getClassName('legend-weekday'),
      children: range(7).map(index => {
        const dayIndex = (index + weekStart) % 7;
        if (!weekdayLabels.byDayIndex(dayIndex)) {
          return null;
        }
        return /*#__PURE__*/jsx("text", {
          x: -LABEL_MARGIN,
          y: labelHeight + (blockSize + blockMargin) * index + blockSize / 2,
          dominantBaseline: "central",
          textAnchor: "end",
          fill: "currentColor",
          children: labels.weekdays[dayIndex]
        }, index);
      })
    });
  }
  function renderMonthLabels() {
    if (!showMonthLabels) {
      return null;
    }
    return /*#__PURE__*/jsx("g", {
      className: getClassName('legend-month'),
      children: getMonthLabels(weeks, labels.months).map(({
        label,
        weekIndex
      }) => /*#__PURE__*/jsx("text", {
        x: (blockSize + blockMargin) * weekIndex,
        y: 0,
        dominantBaseline: "hanging",
        fill: "currentColor",
        children: label
      }, weekIndex))
    });
  }
  function colorForLevel(level) {
    return colorScale[level - minLevel]; // shift to zero-based index
  }
  const {
    width,
    height
  } = getDimensions();
  return /*#__PURE__*/jsxs("article", {
    ref: ref,
    className: `${NAMESPACE} ${className ?? ''}`.trim(),
    style: {
      ...styleProp,
      ...styles.container(fontSize)
    },
    children: [/*#__PURE__*/jsx("div", {
      className: getClassName('scroll-container'),
      style: styles.scrollContainer(fontSize),
      children: /*#__PURE__*/jsxs("svg", {
        width: width,
        height: height,
        viewBox: `0 0 ${width} ${height}`,
        className: getClassName('calendar'),
        style: {
          ...styles.calendar,
          marginLeft: weekdayLabelOffset
        },
        children: [!loading && renderWeekdayLabels(), !loading && renderMonthLabels(), renderCalendar()]
      })
    }), renderFooter()]
  });
});
ActivityCalendar.displayName = 'ActivityCalendar';

export { ActivityCalendar as A, getClassName as g };
//# sourceMappingURL=index-CLOTiFb2.js.map