diff --git a/client/wildcard/src/components/Calendar/Calendar.module.scss b/client/wildcard/src/components/Calendar/Calendar.module.scss
new file mode 100644
index 00000000000..fac4aa84801
--- /dev/null
+++ b/client/wildcard/src/components/Calendar/Calendar.module.scss
@@ -0,0 +1,75 @@
+// stylelint-disable selector-class-pattern
+
+/*
+* The 'react-calendar' is imported without styles but with predefined BEM-classes.
+* This file styles the calendar to follow Sourcegraph designs styles.
+*/
+.container {
+ width: 21rem;
+ display: inline-block;
+
+ :global {
+ .react-calendar {
+ &__navigation__arrow,
+ &__navigation__label,
+ &__tile {
+ background-color: inherit;
+ border: none;
+ padding: 0.25rem;
+ }
+
+ &__navigation {
+ display: flex;
+ margin-bottom: 0.5rem;
+
+ &__arrow {
+ border: 1px solid var(--secondary);
+ margin: 0 0.25rem;
+ border-radius: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ }
+
+ &__label {
+ font-weight: bold;
+ pointer-events: none;
+ }
+ }
+
+ &__month-view__weekdays {
+ &__weekday {
+ text-align: center;
+ font-weight: bold;
+ }
+ }
+
+ &__tile {
+ &--rangeStart {
+ border-top-left-radius: 0.25rem;
+ }
+ &--rangeEnd {
+ border-bottom-right-radius: 0.25rem;
+ }
+ &--rangeBothEnds {
+ border-radius: 0.25rem;
+ }
+ &--active {
+ background-color: var(--primary-2);
+ }
+ &:disabled {
+ color: var(--secondary);
+ }
+ }
+
+ &__month-view__days__day--neighboringMonth {
+ color: var(--text-muted);
+ }
+ }
+ }
+}
+
+.highlight-today :global(.react-calendar__tile--now) {
+ font-weight: 500;
+ text-decoration: underline;
+ text-decoration-thickness: 0.1rem;
+}
diff --git a/client/wildcard/src/components/Calendar/Calendar.story.tsx b/client/wildcard/src/components/Calendar/Calendar.story.tsx
new file mode 100644
index 00000000000..dde17ed895e
--- /dev/null
+++ b/client/wildcard/src/components/Calendar/Calendar.story.tsx
@@ -0,0 +1,109 @@
+import { useState } from 'react'
+
+import { DecoratorFn, Meta, Story } from '@storybook/react'
+import { addDays, startOfDay, subDays } from 'date-fns'
+
+import { BrandedStory } from '@sourcegraph/branded/src/components/BrandedStory'
+import webStyles from '@sourcegraph/web/src/SourcegraphWebApp.scss'
+
+import { Badge, Text } from '..'
+
+import { Calendar } from './Calendar'
+
+const decorator: DecoratorFn = story => (
+
+ {() => (
+
+
+ This is an{' '}
+
+ Experimental
+ {' '}
+ component and built on top of `react-calendar` package with Sourcegraph CSS styling on top. It
+ intentionally, omits other `react-calendar` props/features to not over-complicate and use as simple
+ calendar, in case if we migrate to another calendar library or build our own.
+
+
{story()}
+
+ )}
+
+)
+
+const config: Meta = {
+ title: 'wildcard/Calendar',
+ component: Calendar,
+ decorators: [decorator],
+}
+
+export default config
+
+// NOTE: hardcoded in order to screenshot test the calendar
+const today = startOfDay(new Date('2022-08-22'))
+
+export const Single: Story = () => {
+ const [value, onChange] = useState(today)
+ return
+}
+
+Single.parameters = {
+ chromatic: {
+ enableDarkMode: true,
+ disableSnapshot: false,
+ },
+}
+
+export const Range: Story = () => {
+ const [value, onChange] = useState<[Date, Date]>([subDays(today, 7), today])
+ return
+}
+
+Range.parameters = {
+ chromatic: {
+ enableDarkMode: true,
+ disableSnapshot: false,
+ },
+}
+
+export const MinMaxDates: Story = () => {
+ const [value, onChange] = useState<[Date, Date]>([subDays(today, 7), today])
+ return (
+
+ )
+}
+
+MinMaxDates.parameters = {
+ chromatic: {
+ enableDarkMode: true,
+ disableSnapshot: false,
+ },
+}
+
+export const HighlightToday: Story = () => {
+ const [value, onChange] = useState(new Date())
+ return
+}
+
+HighlightToday.parameters = {
+ chromatic: {
+ enableDarkMode: true,
+ disableSnapshot: true,
+ },
+}
+
+export const HighlightTodayRange: Story = () => {
+ const [value, onChange] = useState<[Date, Date]>([subDays(new Date(), 4), addDays(new Date(), 3)])
+ return
+}
+
+HighlightTodayRange.parameters = {
+ chromatic: {
+ enableDarkMode: true,
+ disableSnapshot: true,
+ },
+}
diff --git a/client/wildcard/src/components/Calendar/Calendar.tsx b/client/wildcard/src/components/Calendar/Calendar.tsx
new file mode 100644
index 00000000000..0761dafe640
--- /dev/null
+++ b/client/wildcard/src/components/Calendar/Calendar.tsx
@@ -0,0 +1,62 @@
+import classNames from 'classnames'
+import ReactCalendar from 'react-calendar'
+
+import { Container } from '@sourcegraph/wildcard'
+
+import styles from './Calendar.module.scss'
+
+interface CalendarDateProps {
+ isRange?: false
+ value?: Date | null
+ onChange: (value: Date) => void
+}
+
+interface CalendarDateRangeProps {
+ isRange: true
+ value?: [Date | null, Date | null] | null
+ onChange: (value: [Date, Date]) => void
+}
+
+type CalendarProps = {
+ className?: string
+ maxDate?: Date
+ minDate?: Date
+ highlightToday?: boolean
+} & (CalendarDateRangeProps | CalendarDateProps)
+
+/**
+ * Renders a calendar component which supports single date or range selection.
+ *
+ * **NOTE:** This is an `EXPERIMENTAL` component and built on top of `react-calendar` package with Sourcegraph CSS styling on top.
+ * It intentionally, omits other `react-calendar` props/features to not over-complicate and use as simple calendar, in case if we migrate to another calendar library or build our own.
+ *
+ * Depending on `isRange` value `true | false`, the component will render a single or range selection calendar as well as infer correct TS props for the range selection.
+ *
+ */
+export const Calendar: React.FunctionComponent = ({
+ className,
+ value,
+ onChange,
+ isRange,
+ minDate,
+ maxDate,
+ highlightToday,
+}) => (
+
+
+
+)
diff --git a/client/wildcard/src/components/Calendar/index.ts b/client/wildcard/src/components/Calendar/index.ts
new file mode 100644
index 00000000000..8c50cf89e87
--- /dev/null
+++ b/client/wildcard/src/components/Calendar/index.ts
@@ -0,0 +1 @@
+export * from './Calendar'
diff --git a/client/wildcard/src/components/index.ts b/client/wildcard/src/components/index.ts
index f1218bf3991..bd745d4aa25 100644
--- a/client/wildcard/src/components/index.ts
+++ b/client/wildcard/src/components/index.ts
@@ -2,6 +2,7 @@
export { Button, ButtonGroup, BUTTON_SIZES } from './Button'
export type { ButtonGroupProps } from './Button'
export { Alert, AlertLink } from './Alert'
+export { Calendar } from './Calendar'
export { Container } from './Container'
export { LineChart, BarChart, PieChart, LegendList, LegendItem, ScrollBox, ParentSize } from './Charts'
export {
diff --git a/package.json b/package.json
index b863c94ae3b..efc2cce6ca8 100644
--- a/package.json
+++ b/package.json
@@ -202,6 +202,7 @@
"@types/pollyjs__persister-fs": "2.0.1",
"@types/puppeteer": "^5.4.5",
"@types/react": "18.0.8",
+ "@types/react-calendar": "^3.5.2",
"@types/react-circular-progressbar": "1.0.2",
"@types/react-dom": "18.0.2",
"@types/react-grid-layout": "1.3.0",
@@ -449,6 +450,7 @@
"pretty-bytes": "^5.3.0",
"prop-types": "^15.7.2",
"react": "18.1.0",
+ "react-calendar": "^3.7.0",
"react-circular-progressbar": "^2.0.3",
"react-dom": "18.1.0",
"react-dom-confetti": "^0.1.4",
diff --git a/yarn.lock b/yarn.lock
index 6e9e74ccbd5..d9421b18528 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6079,6 +6079,13 @@
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz#fa8e1ad1d474688a757140c91de6dace6f4abc8d"
integrity sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==
+"@types/react-calendar@^3.5.2":
+ version "3.5.2"
+ resolved "https://registry.npmjs.org/@types/react-calendar/-/react-calendar-3.5.2.tgz#e401034e4bb82f4510ba87aa490e98b5746e16e0"
+ integrity sha512-8gkU9KaE33VVbu3YWvxXjEk4BsalgSYR3c/5XF9XNJiQ/2MKxiGkTg/PfOHUX/BvcADykRBMAEJiCi6jFPEE3A==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-circular-progressbar@1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@types/react-circular-progressbar/-/react-circular-progressbar-1.0.2.tgz#a23e2d0f4e14a89d75c7c2286f8e96702f9862f2"
@@ -6978,6 +6985,11 @@
resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz#b5fde2f0f79c1e120307c415a4c1d5eb15a6f278"
integrity sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==
+"@wojtekmaj/date-utils@^1.0.2":
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz#2dcfd92881425c5923e429c2aec86fb3609032a1"
+ integrity sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==
+
"@wry/context@^0.6.0":
version "0.6.0"
resolved "https://registry.npmjs.org/@wry/context/-/context-0.6.0.tgz#f903eceb89d238ef7e8168ed30f4511f92d83e06"
@@ -13680,6 +13692,13 @@ get-uri@3:
fs-extra "^8.1.0"
ftp "^0.3.10"
+get-user-locale@^1.2.0:
+ version "1.5.1"
+ resolved "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.5.1.tgz#18a9ba2cfeed0e713ea00968efa75d620523a5ea"
+ integrity sha512-WiNpoFRcHn1qxP9VabQljzGwkAQDrcpqUtaP0rNBEkFxJdh4f3tik6MfZsMYZc+UgQJdGCxWEjL9wnCUlRQXag==
+ dependencies:
+ lodash.memoize "^4.1.1"
+
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -17521,7 +17540,7 @@ lodash.isstring@^4.0.1:
resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
-lodash.memoize@^4.1.2:
+lodash.memoize@^4.1.1, lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
@@ -18009,6 +18028,11 @@ meow@^9.0.0:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
+merge-class-names@^1.1.1:
+ version "1.4.2"
+ resolved "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz#78d6d95ab259e7e647252a7988fd25a27d5a8835"
+ integrity sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -20863,6 +20887,16 @@ rc@1.2.8, rc@^1.2.7, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-calendar@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.npmjs.org/react-calendar/-/react-calendar-3.7.0.tgz#951d56e91afb33b1c1e019cb790349fbffcc6894"
+ integrity sha512-zkK95zWLWLC6w3O7p3SHx/FJXEyyD2UMd4jr3CrKD+G73N+G5vEwrXxYQCNivIPoFNBjqoyYYGlkHA+TBDPLCw==
+ dependencies:
+ "@wojtekmaj/date-utils" "^1.0.2"
+ get-user-locale "^1.2.0"
+ merge-class-names "^1.1.1"
+ prop-types "^15.6.0"
+
react-circular-progressbar@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz#fa8eb59f8db168d2904bae4590641792c80f5991"