From c385853e004faaa70883e2787107809eaf9ff6ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 21:37:33 +0000 Subject: [PATCH] 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. --- .../ApiKeyNew/components/ExpiryDatePicker.tsx | 23 +-- app/scenes/ApiKeyNew/index.tsx | 1 - shared/components/Calendar.tsx | 167 ++++++++++++++++++ shared/editor/components/Mentions.tsx | 147 +-------------- 4 files changed, 173 insertions(+), 165 deletions(-) create mode 100644 shared/components/Calendar.tsx 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. */} -