diff --git a/app/scenes/ApiKeyNew/components/ExpiryDatePicker.tsx b/app/scenes/ApiKeyNew/components/ExpiryDatePicker.tsx
index 55c145a35b..eee6cdfc0c 100644
--- a/app/scenes/ApiKeyNew/components/ExpiryDatePicker.tsx
+++ b/app/scenes/ApiKeyNew/components/ExpiryDatePicker.tsx
@@ -1,9 +1,9 @@
import { format as formatDate } from "date-fns";
import { CalendarIcon } from "outline-icons";
import * as React from "react";
-import { DayPicker } from "react-day-picker";
import { useTranslation } from "react-i18next";
-import styled, { useTheme } from "styled-components";
+import styled from "styled-components";
+import { Calendar } from "@shared/components/Calendar";
import { dateLocale } from "@shared/utils/date";
import Button from "~/components/Button";
import {
@@ -21,25 +21,10 @@ type Props = {
const ExpiryDatePicker = ({ selectedDate, onSelect }: Props) => {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
- const theme = useTheme();
const userLocale = useUserLocale();
const locale = dateLocale(userLocale);
- const styles = React.useMemo(
- () =>
- ({
- "--rdp-caption-font-size": "16px",
- "--rdp-cell-size": "34px",
- "--rdp-selected-text": theme.accentText,
- "--rdp-accent-color": theme.accent,
- "--rdp-accent-color-dark": theme.accent,
- "--rdp-background-color": theme.listItemHoverBackground,
- "--rdp-background-color-dark": theme.listItemHoverBackground,
- }) as React.CSSProperties,
- [theme]
- );
-
const handleSelect = React.useCallback(
(date: Date) => {
setOpen(false);
@@ -63,12 +48,12 @@ const ExpiryDatePicker = ({ selectedDate, onSelect }: Props) => {
side="right"
shrink
>
-
diff --git a/app/scenes/ApiKeyNew/index.tsx b/app/scenes/ApiKeyNew/index.tsx
index 8e31af2876..c2c503a347 100644
--- a/app/scenes/ApiKeyNew/index.tsx
+++ b/app/scenes/ApiKeyNew/index.tsx
@@ -13,7 +13,6 @@ import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import useUserLocale from "~/hooks/useUserLocale";
import { dateToExpiry } from "~/utils/date";
-import "react-day-picker/dist/style.css";
import ExpiryDatePicker from "./components/ExpiryDatePicker";
import { ExpiryType, ExpiryValues, calculateExpiryDate } from "./utils";
diff --git a/shared/components/Calendar.tsx b/shared/components/Calendar.tsx
new file mode 100644
index 0000000000..c45568e517
--- /dev/null
+++ b/shared/components/Calendar.tsx
@@ -0,0 +1,167 @@
+import * as React from "react";
+import { DayPicker } from "react-day-picker";
+import styled from "styled-components";
+import { s } from "../styles";
+
+type Props = React.ComponentProps;
+
+/**
+ * A themed calendar built on react-day-picker. It is styled from scratch (the
+ * library's base stylesheet is intentionally not relied upon) so that it looks
+ * consistent everywhere it is used. Outside (previous/next month) days are
+ * shown de-emphasised, the selected day is a solid accent-filled circle, and
+ * today is highlighted with the accent colour.
+ *
+ * @param props the underlying react-day-picker props; `showOutsideDays` and
+ * `fixedWeeks` default to true but may be overridden.
+ * @returns the rendered calendar.
+ */
+export function Calendar(props: Props) {
+ return (
+
+
+
+ );
+}
+
+const Wrapper = styled.div`
+ padding: 12px;
+ color: ${s("text")};
+
+ .rdp {
+ margin: 0;
+ }
+
+ /* Visually-hidden accessibility labels (would otherwise show without the
+ base stylesheet). */
+ .rdp-vhidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+ appearance: none;
+ }
+
+ .rdp-month {
+ width: 100%;
+ }
+
+ .rdp-caption {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 2px 8px;
+ }
+
+ .rdp-caption_label {
+ font-size: 14px;
+ font-weight: 600;
+ color: ${s("text")};
+ }
+
+ .rdp-nav {
+ display: flex;
+ gap: 2px;
+ }
+
+ .rdp-nav_button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ border: 0;
+ background: none;
+ border-radius: 4px;
+ color: ${s("textSecondary")};
+ cursor: pointer;
+ transition: background 100ms ease;
+
+ &:hover {
+ background: ${s("listItemHoverBackground")};
+ }
+ }
+
+ .rdp-nav_icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ .rdp-table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+
+ .rdp-head_cell {
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: none;
+ color: ${s("textTertiary")};
+ padding: 4px 0;
+ text-align: center;
+ }
+
+ .rdp-cell {
+ padding: 1px;
+ text-align: center;
+ }
+
+ .rdp-day {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ border: 0;
+ background: none;
+ border-radius: 50%;
+ font-family: inherit;
+ font-size: 13px;
+ font-variant-numeric: tabular-nums;
+ color: ${s("text")};
+ cursor: pointer;
+ transition: background 100ms ease;
+
+ &:hover:not([disabled]):not(.rdp-day_selected) {
+ background: ${s("listItemHoverBackground")};
+ }
+
+ &:focus-visible:not([disabled]) {
+ outline: 2px solid ${s("accent")};
+ outline-offset: -2px;
+ }
+ }
+
+ /* Today, when not selected, is emphasised with the accent colour. */
+ .rdp-day_today:not(.rdp-day_selected) {
+ font-weight: 700;
+ color: ${s("accent")};
+ }
+
+ /* Days belonging to the previous/next month are clearly de-emphasised. */
+ .rdp-day_outside {
+ color: ${s("textTertiary")};
+ opacity: 0.5;
+ }
+
+ .rdp-day[disabled] {
+ color: ${s("textTertiary")};
+ opacity: 0.4;
+ cursor: default;
+ }
+
+ /* The selected day is a solid accent-filled circle. */
+ .rdp-day_selected,
+ .rdp-day_selected:hover,
+ .rdp-day_selected:focus-visible {
+ background: ${s("accent")};
+ color: ${s("accentText")};
+ font-weight: 500;
+ opacity: 1;
+ }
+`;
diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx
index 8c60b7838b..2d68497727 100644
--- a/shared/editor/components/Mentions.tsx
+++ b/shared/editor/components/Mentions.tsx
@@ -9,7 +9,6 @@ import {
} from "outline-icons";
import type { Node } from "prosemirror-model";
import * as React from "react";
-import { DayPicker } from "react-day-picker";
import { useTranslation } from "react-i18next";
import { RemoveScroll } from "react-remove-scroll";
import { Link } from "react-router-dom";
@@ -23,6 +22,7 @@ import {
toISODate,
} from "../../utils/date";
import { Backticks } from "../../components/Backticks";
+import { Calendar } from "../../components/Calendar";
import Flex from "../../components/Flex";
import Icon from "../../components/Icon";
import { IssueStatusIcon } from "../../components/IssueStatusIcon";
@@ -582,11 +582,9 @@ export const MentionDate = observer(function MentionDate_(props: DateProps) {
{/* Lock page scroll while open, matching the inline editor menu. */}
-