Share one themed Calendar between the date mention and API key pickers

Extract the custom react-day-picker styling into a reusable Calendar
component and use it in both the date mention picker and the API key
expiry picker, so they look identical. The calendar owns its own padding
and the API key scene no longer needs the library's base stylesheet.
This commit is contained in:
Claude
2026-06-07 21:37:33 +00:00
parent 61ca11e919
commit c385853e00
4 changed files with 173 additions and 165 deletions
@@ -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
>
<DayPicker
<Calendar
required
mode="single"
selected={selectedDate}
onSelect={handleSelect}
style={styles}
locale={locale}
disabled={{ before: new Date() }}
/>
</PopoverContent>
-1
View File
@@ -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";
+167
View File
@@ -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<typeof DayPicker>;
/**
* 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 (
<Wrapper>
<DayPicker showOutsideDays fixedWeeks {...props} />
</Wrapper>
);
}
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;
}
`;
+2 -145
View File
@@ -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. */}
<RemoveScroll as={Slot} allowPinchZoom>
<DatePopoverContent>
<DayPicker
<Calendar
required
mode="single"
showOutsideDays
fixedWeeks
selected={selectedDate}
defaultMonth={selectedDate}
onSelect={handleSelect}
@@ -633,8 +631,6 @@ const DatePopoverContent = styled.div`
box-shadow: ${s("menuShadow")};
border-radius: 8px;
outline: none;
padding: 12px;
color: ${s("text")};
&[data-state="open"] {
animation: fadeIn 150ms ease;
@@ -648,145 +644,6 @@ const DatePopoverContent = styled.div`
opacity: 1;
}
}
/* react-day-picker is styled from scratch here as its base stylesheet is
not loaded in the editor; this gives us full control over the look. */
.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;
}
`;
const StyledWarningIcon = styled(WarningIcon)`