mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
refactor(site): replace react-date-range with shadcn Calendar + DateRangePicker (#23495)
This commit is contained in:
+1
-2
@@ -105,7 +105,7 @@
|
||||
"react": "19.2.2",
|
||||
"react-color": "2.19.3",
|
||||
"react-confetti": "6.4.0",
|
||||
"react-date-range": "1.4.0",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.2",
|
||||
"react-markdown": "9.1.0",
|
||||
"react-query": "npm:@tanstack/react-query@5.77.0",
|
||||
@@ -162,7 +162,6 @@
|
||||
"@types/novnc__novnc": "1.5.0",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-color": "3.0.13",
|
||||
"@types/react-date-range": "1.4.4",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-syntax-highlighter": "15.5.13",
|
||||
"@types/react-virtualized-auto-sizer": "1.0.8",
|
||||
|
||||
Generated
+31
-50
@@ -223,9 +223,9 @@ importers:
|
||||
react-confetti:
|
||||
specifier: 6.4.0
|
||||
version: 6.4.0(react@19.2.2)
|
||||
react-date-range:
|
||||
specifier: 1.4.0
|
||||
version: 1.4.0(date-fns@2.30.0)(react@19.2.2)
|
||||
react-day-picker:
|
||||
specifier: 9.14.0
|
||||
version: 9.14.0(react@19.2.2)
|
||||
react-dom:
|
||||
specifier: 19.2.2
|
||||
version: 19.2.2(react@19.2.2)
|
||||
@@ -389,9 +389,6 @@ importers:
|
||||
'@types/react-color':
|
||||
specifier: 3.0.13
|
||||
version: 3.0.13(@types/react@19.2.7)
|
||||
'@types/react-date-range':
|
||||
specifier: 1.4.4
|
||||
version: 1.4.4
|
||||
'@types/react-dom':
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3(@types/react@19.2.7)
|
||||
@@ -878,6 +875,9 @@ packages:
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==, tarball: https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==, tarball: https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz}
|
||||
|
||||
'@emnapi/core@1.7.1':
|
||||
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz}
|
||||
|
||||
@@ -2579,6 +2579,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@swc/core': '*'
|
||||
|
||||
'@tabby_ai/hijri-converter@1.0.5':
|
||||
resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==, tarball: https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
'@tailwindcss/typography@0.5.19':
|
||||
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==, tarball: https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz}
|
||||
peerDependencies:
|
||||
@@ -2901,9 +2905,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
|
||||
'@types/react-date-range@1.4.4':
|
||||
resolution: {integrity: sha512-9Y9NyNgaCsEVN/+O4HKuxzPbVjRVBGdOKRxMDcsTRWVG62lpYgnxefNckTXDWup8FvczoqPW0+ESZR6R1yymDg==, tarball: https://registry.npmjs.org/@types/react-date-range/-/react-date-range-1.4.4.tgz}
|
||||
|
||||
'@types/react-dom@18.3.7':
|
||||
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==, tarball: https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz}
|
||||
peerDependencies:
|
||||
@@ -3504,9 +3505,6 @@ packages:
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==, tarball: https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz}
|
||||
|
||||
classnames@2.3.2:
|
||||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==, tarball: https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz}
|
||||
|
||||
cli-cursor@3.1.0:
|
||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==, tarball: https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3851,9 +3849,11 @@ packages:
|
||||
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==, tarball: https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, tarball: https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz}
|
||||
engines: {node: '>=0.11'}
|
||||
date-fns-jalali@4.1.0-0:
|
||||
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==, tarball: https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==, tarball: https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz}
|
||||
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==, tarball: https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz}
|
||||
@@ -5898,11 +5898,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-date-range@1.4.0:
|
||||
resolution: {integrity: sha512-+9t0HyClbCqw1IhYbpWecjsiaftCeRN5cdhsi9v06YdimwyMR2yYHWcgVn3URwtN/txhqKpEZB6UX1fHpvK76w==, tarball: https://registry.npmjs.org/react-date-range/-/react-date-range-1.4.0.tgz}
|
||||
react-day-picker@9.14.0:
|
||||
resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==, tarball: https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
date-fns: 2.0.0-alpha.7 || >=2.0.0
|
||||
react: ^0.14 || ^15.0.0-rc || >=15.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-docgen-typescript@2.4.0:
|
||||
resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==, tarball: https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz}
|
||||
@@ -5943,11 +5943,6 @@ packages:
|
||||
react-is@19.1.1:
|
||||
resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==, tarball: https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz}
|
||||
|
||||
react-list@0.8.17:
|
||||
resolution: {integrity: sha512-pgmzGi0G5uGrdHzMhgO7KR1wx5ZXVvI3SsJUmkblSAKtewIhMwbQiMuQiTE83ozo04BQJbe0r3WIWzSO0dR1xg==, tarball: https://registry.npmjs.org/react-list/-/react-list-0.8.17.tgz}
|
||||
peerDependencies:
|
||||
react: 0.14 || 15 - 18
|
||||
|
||||
react-markdown@9.1.0:
|
||||
resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==, tarball: https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz}
|
||||
peerDependencies:
|
||||
@@ -6264,9 +6259,6 @@ packages:
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, tarball: https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz}
|
||||
|
||||
shallow-equal@1.2.1:
|
||||
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==, tarball: https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7605,6 +7597,8 @@ snapshots:
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@emnapi/core@1.7.1':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.1.0
|
||||
@@ -9442,6 +9436,8 @@ snapshots:
|
||||
'@swc/counter': 0.1.3
|
||||
jsonc-parser: 3.2.0
|
||||
|
||||
'@tabby_ai/hijri-converter@1.0.5': {}
|
||||
|
||||
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.18(yaml@2.7.0))':
|
||||
dependencies:
|
||||
postcss-selector-parser: 6.0.10
|
||||
@@ -9825,11 +9821,6 @@ snapshots:
|
||||
'@types/react': 19.2.7
|
||||
'@types/reactcss': 1.2.13(@types/react@19.2.7)
|
||||
|
||||
'@types/react-date-range@1.4.4':
|
||||
dependencies:
|
||||
'@types/react': 19.2.7
|
||||
date-fns: 2.30.0
|
||||
|
||||
'@types/react-dom@18.3.7(@types/react@19.2.7)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.7
|
||||
@@ -10501,8 +10492,6 @@ snapshots:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
||||
classnames@2.3.2: {}
|
||||
|
||||
cli-cursor@3.1.0:
|
||||
dependencies:
|
||||
restore-cursor: 3.1.0
|
||||
@@ -10861,9 +10850,9 @@ snapshots:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 15.1.0
|
||||
|
||||
date-fns@2.30.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
date-fns-jalali@4.1.0-0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
|
||||
@@ -13527,14 +13516,13 @@ snapshots:
|
||||
react: 19.2.2
|
||||
tween-functions: 1.2.0
|
||||
|
||||
react-date-range@1.4.0(date-fns@2.30.0)(react@19.2.2):
|
||||
react-day-picker@9.14.0(react@19.2.2):
|
||||
dependencies:
|
||||
classnames: 2.3.2
|
||||
date-fns: 2.30.0
|
||||
prop-types: 15.8.1
|
||||
'@date-fns/tz': 1.4.1
|
||||
'@tabby_ai/hijri-converter': 1.0.5
|
||||
date-fns: 4.1.0
|
||||
date-fns-jalali: 4.1.0-0
|
||||
react: 19.2.2
|
||||
react-list: 0.8.17(react@19.2.2)
|
||||
shallow-equal: 1.2.1
|
||||
|
||||
react-docgen-typescript@2.4.0(typescript@5.6.3):
|
||||
dependencies:
|
||||
@@ -13578,11 +13566,6 @@ snapshots:
|
||||
|
||||
react-is@19.1.1: {}
|
||||
|
||||
react-list@0.8.17(react@19.2.2):
|
||||
dependencies:
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.2
|
||||
|
||||
react-markdown@9.1.0(@types/react@19.2.7)(react@19.2.2):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -13991,8 +13974,6 @@ snapshots:
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shallow-equal@1.2.1: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
@@ -52,12 +52,12 @@ import { useSearchParams } from "react-router";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { formatTokenCount } from "utils/analytics";
|
||||
import { formatCostMicros } from "utils/currency";
|
||||
import {
|
||||
DateRange,
|
||||
type DateRangeValue,
|
||||
} from "../TemplatePage/TemplateInsightsPage/DateRange";
|
||||
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
|
||||
import { ChatModelAdminPanel } from "./components/ChatModelAdminPanel/ChatModelAdminPanel";
|
||||
import {
|
||||
DateRangePicker,
|
||||
type DateRangeValue,
|
||||
} from "./components/DateRangePicker/DateRangePicker";
|
||||
import { InsightsContent } from "./components/InsightsContent";
|
||||
import { LimitsTab } from "./components/LimitsTab";
|
||||
import { MCPServerAdminPanel } from "./components/MCPServerAdminPanel";
|
||||
@@ -260,7 +260,10 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
|
||||
}
|
||||
badge={<AdminBadge />}
|
||||
action={
|
||||
<DateRange value={displayDateRange} onChange={onDateRangeChange} />
|
||||
<DateRangePicker
|
||||
value={displayDateRange}
|
||||
onChange={onDateRangeChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useState } from "react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
import { Calendar } from "./Calendar";
|
||||
|
||||
const meta: Meta<typeof Calendar> = {
|
||||
title: "components/Calendar",
|
||||
component: Calendar,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="rounded-lg border border-solid border-border-default w-fit">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Calendar>;
|
||||
|
||||
export const Single: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
selected: new Date("2025-03-15"),
|
||||
},
|
||||
};
|
||||
|
||||
export const Range: Story = {
|
||||
render: function RangeStory() {
|
||||
const [range, setRange] = useState<DateRange>({
|
||||
from: new Date("2025-03-10"),
|
||||
to: new Date("2025-03-18"),
|
||||
});
|
||||
return (
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={range}
|
||||
onSelect={(r) => r && setRange(r)}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TwoMonths: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
numberOfMonths: 2,
|
||||
selected: new Date("2025-03-15"),
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledFutureDates: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
selected: new Date("2025-03-15"),
|
||||
disabled: { after: new Date("2025-03-20") },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Adapted from shadcn/ui on 2025-03-24.
|
||||
* @see {@link https://ui.shadcn.com/docs/components/calendar}
|
||||
*
|
||||
* Built on top of React DayPicker v9. Styled with Tailwind using the
|
||||
* project's existing design tokens so it matches every other primitive
|
||||
* in the component library.
|
||||
*/
|
||||
|
||||
import { Button, type ButtonProps } from "components/Button/Button";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import {
|
||||
type DayButton,
|
||||
DayPicker,
|
||||
getDefaultClassNames,
|
||||
} from "react-day-picker";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "subtle",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: ButtonProps["variant"];
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-surface-primary group/calendar p-3 [--cell-size:2rem]",
|
||||
className,
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"relative flex flex-col gap-4 md:flex-row",
|
||||
defaultClassNames.months,
|
||||
),
|
||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||
defaultClassNames.nav,
|
||||
),
|
||||
button_previous: cn(
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0",
|
||||
"inline-flex items-center justify-center rounded-md",
|
||||
"bg-transparent border-0 cursor-pointer",
|
||||
"text-content-secondary hover:text-content-primary hover:bg-surface-secondary",
|
||||
"aria-disabled:opacity-50",
|
||||
defaultClassNames.button_previous,
|
||||
),
|
||||
button_next: cn(
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0",
|
||||
"inline-flex items-center justify-center rounded-md",
|
||||
"bg-transparent border-0 cursor-pointer",
|
||||
"text-content-secondary hover:text-content-primary hover:bg-surface-secondary",
|
||||
"aria-disabled:opacity-50",
|
||||
defaultClassNames.button_next,
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
|
||||
defaultClassNames.month_caption,
|
||||
),
|
||||
dropdowns: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
defaultClassNames.dropdowns,
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"has-focus:border-content-link border-border-default relative rounded-md border",
|
||||
defaultClassNames.dropdown_root,
|
||||
),
|
||||
dropdown: cn(
|
||||
"bg-surface-primary absolute inset-0 opacity-0",
|
||||
defaultClassNames.dropdown,
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium text-sm text-content-primary",
|
||||
defaultClassNames.caption_label,
|
||||
),
|
||||
table:
|
||||
"w-full border-collapse border-0 [&_td]:border-0 [&_th]:border-0",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-content-secondary flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||
defaultClassNames.weekday,
|
||||
),
|
||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"w-[--cell-size] select-none",
|
||||
defaultClassNames.week_number_header,
|
||||
),
|
||||
week_number: cn(
|
||||
"text-content-secondary select-none text-[0.8rem]",
|
||||
defaultClassNames.week_number,
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full select-none p-0 text-center",
|
||||
"[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
"[&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||
defaultClassNames.day,
|
||||
),
|
||||
range_start: cn(
|
||||
"bg-surface-tertiary rounded-l-md",
|
||||
defaultClassNames.range_start,
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn(
|
||||
"bg-surface-tertiary rounded-r-md",
|
||||
defaultClassNames.range_end,
|
||||
),
|
||||
today: cn(
|
||||
"bg-surface-tertiary text-content-primary rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today,
|
||||
),
|
||||
outside: cn(
|
||||
"text-content-disabled aria-selected:text-content-disabled",
|
||||
defaultClassNames.outside,
|
||||
),
|
||||
disabled: cn(
|
||||
"text-content-disabled opacity-50",
|
||||
defaultClassNames.disabled,
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Chevron: ({
|
||||
className,
|
||||
orientation,
|
||||
size: _size,
|
||||
disabled: _disabled,
|
||||
...rest
|
||||
}) => {
|
||||
const Icon =
|
||||
orientation === "left" ? ChevronLeftIcon : ChevronRightIcon;
|
||||
return <Icon className={cn("size-4", className)} {...rest} />;
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...weekProps }) => (
|
||||
<td {...weekProps}>
|
||||
<div className="flex size-[--cell-size] items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
),
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none",
|
||||
"data-[selected-single=true]:bg-surface-invert-primary data-[selected-single=true]:text-content-invert",
|
||||
"data-[range-middle=true]:bg-surface-tertiary data-[range-middle=true]:text-content-primary",
|
||||
"data-[range-start=true]:bg-surface-invert-primary data-[range-start=true]:text-content-invert",
|
||||
"data-[range-end=true]:bg-surface-invert-primary data-[range-end=true]:text-content-invert",
|
||||
"data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md",
|
||||
"group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10",
|
||||
"group-data-[focused=true]/day:ring-2 group-data-[focused=true]/day:ring-content-link",
|
||||
defaultClassNames.day,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { DateRangePicker, type DateRangeValue } from "./DateRangePicker";
|
||||
|
||||
const fixedNow = dayjs("2025-03-15T12:00:00Z");
|
||||
|
||||
const defaultValue: DateRangeValue = {
|
||||
startDate: fixedNow.subtract(30, "day").toDate(),
|
||||
endDate: fixedNow.toDate(),
|
||||
};
|
||||
|
||||
const meta: Meta<typeof DateRangePicker> = {
|
||||
title: "components/DateRangePicker",
|
||||
component: DateRangePicker,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DateRangePicker>;
|
||||
|
||||
export const Closed: Story = {
|
||||
args: {
|
||||
value: defaultValue,
|
||||
onChange: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export const Open: Story = {
|
||||
args: {
|
||||
value: defaultValue,
|
||||
onChange: () => {},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const trigger = canvas.getByRole("button");
|
||||
await userEvent.click(trigger);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Last 7 days")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// All preset labels should be visible.
|
||||
expect(screen.getByText("Today")).toBeInTheDocument();
|
||||
expect(screen.getByText("Yesterday")).toBeInTheDocument();
|
||||
expect(screen.getByText("Last 14 days")).toBeInTheDocument();
|
||||
expect(screen.getByText("Last 30 days")).toBeInTheDocument();
|
||||
|
||||
// The selected range should be displayed above the calendar.
|
||||
// The start date appears in both the trigger and the range
|
||||
// display, so expect two instances.
|
||||
const startDateLabel = dayjs(defaultValue.startDate).format("MMM D, YYYY");
|
||||
expect(screen.getAllByText(startDateLabel)).toHaveLength(2);
|
||||
|
||||
// Cancel and Apply buttons should be visible.
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Apply" })).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectPreset: Story = {
|
||||
render: function SelectPresetStory() {
|
||||
const [value, setValue] = useState<DateRangeValue>(defaultValue);
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
|
||||
const trigger = canvas.getByRole("button");
|
||||
await userEvent.click(trigger);
|
||||
|
||||
const preset = await body.findByText("Last 7 days");
|
||||
await userEvent.click(preset);
|
||||
|
||||
// Popover should close after selecting a preset.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Last 7 days")).toBeNull();
|
||||
});
|
||||
|
||||
// The trigger button text should have changed to reflect a
|
||||
// narrower range than the original 30-day default.
|
||||
const updatedTrigger = canvas.getByRole("button");
|
||||
expect(updatedTrigger.textContent).not.toContain(
|
||||
dayjs(defaultValue.startDate).format("MMM D, YYYY"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectCalendarRange: Story = {
|
||||
render: function SelectCalendarRangeStory() {
|
||||
const [value, setValue] = useState<DateRangeValue>({
|
||||
startDate: new Date("2025-03-01"),
|
||||
endDate: new Date("2025-03-15"),
|
||||
});
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
|
||||
// Wait for the calendar to render.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Today")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The calendar should render day cells.
|
||||
const dayButtons = body.getAllByRole("gridcell");
|
||||
expect(dayButtons.length).toBeGreaterThan(0);
|
||||
|
||||
// Apply button should be disabled until the range changes.
|
||||
const applyButton = screen.getByRole("button", { name: "Apply" });
|
||||
expect(applyButton).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const CancelClosesWithoutApplying: Story = {
|
||||
render: function CancelStory() {
|
||||
const [value, setValue] = useState<DateRangeValue>(defaultValue);
|
||||
return <DateRangePicker value={value} onChange={setValue} />;
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const trigger = canvas.getByRole("button");
|
||||
const originalText = trigger.textContent;
|
||||
await userEvent.click(trigger);
|
||||
|
||||
// Click Cancel.
|
||||
const cancelButton = await screen.findByRole("button", {
|
||||
name: "Cancel",
|
||||
});
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
// Popover should close.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Cancel")).toBeNull();
|
||||
});
|
||||
|
||||
// The trigger text should remain unchanged.
|
||||
expect(canvas.getByRole("button").textContent).toBe(originalText);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomPresets: Story = {
|
||||
args: {
|
||||
value: defaultValue,
|
||||
onChange: () => {},
|
||||
presets: [
|
||||
{
|
||||
label: "This week",
|
||||
range: () => ({
|
||||
from: dayjs().startOf("week").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "This month",
|
||||
range: () => ({
|
||||
from: dayjs().startOf("month").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("This week")).toBeInTheDocument();
|
||||
expect(screen.getByText("This month")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Default presets should not be present.
|
||||
expect(screen.queryByText("Last 7 days")).toBeNull();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* A date-range picker composed from the project's Calendar, Popover, and
|
||||
* Button primitives. Replaces the legacy react-date-range based DateRange
|
||||
* component with one that matches the native design language.
|
||||
*/
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import dayjs from "dayjs";
|
||||
import { CalendarIcon, MoveRightIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import type { DateRange as DayPickerDateRange } from "react-day-picker";
|
||||
import { cn } from "utils/cn";
|
||||
import { Calendar } from "../Calendar/Calendar";
|
||||
|
||||
export type DateRangeValue = {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
};
|
||||
|
||||
interface DateRangePreset {
|
||||
label: string;
|
||||
range: () => { from: Date; to: Date };
|
||||
}
|
||||
|
||||
const defaultPresets: DateRangePreset[] = [
|
||||
{
|
||||
label: "Today",
|
||||
range: () => ({ from: new Date(), to: new Date() }),
|
||||
},
|
||||
{
|
||||
label: "Yesterday",
|
||||
range: () => {
|
||||
const d = dayjs().subtract(1, "day").toDate();
|
||||
return { from: d, to: d };
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(6, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(13, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
range: () => ({
|
||||
from: dayjs().subtract(29, "day").toDate(),
|
||||
to: new Date(),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
interface DateRangePickerProps {
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
presets?: DateRangePreset[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a calendar selection into the API-friendly boundary format
|
||||
* that the old component produced: startDate at midnight, endDate either
|
||||
* rounded up to the next hour (if it falls on today) or to the start of
|
||||
* the following day.
|
||||
*/
|
||||
function toBoundary(from: Date, to: Date): DateRangeValue {
|
||||
const start = dayjs(from).startOf("day").toDate();
|
||||
const end = dayjs(to).isSame(dayjs(), "day")
|
||||
? dayjs().startOf("hour").add(1, "hour").toDate()
|
||||
: dayjs(to).startOf("day").add(1, "day").toDate();
|
||||
return { startDate: start, endDate: end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the boundary normalization so the calendar highlights the
|
||||
* inclusive end date the user originally selected, not the exclusive
|
||||
* API boundary. Midnight boundaries get shifted back by one day;
|
||||
* sub-day boundaries (today's rounded-up hour) stay on the same day.
|
||||
*/
|
||||
function fromBoundary(value: DateRangeValue): DayPickerDateRange {
|
||||
const from = dayjs(value.startDate).startOf("day").toDate();
|
||||
const endDayjs = dayjs(value.endDate);
|
||||
const to = endDayjs.isSame(endDayjs.startOf("day"))
|
||||
? endDayjs.subtract(1, "day").toDate()
|
||||
: endDayjs.toDate();
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
export const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
presets = defaultPresets,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Internal selection state kept separate from the committed value
|
||||
// so the user can freely adjust the range before applying. This
|
||||
// uses raw calendar dates (inclusive), not the API boundary format.
|
||||
const [selection, setSelection] = useState<DayPickerDateRange | undefined>(
|
||||
() => fromBoundary(value),
|
||||
);
|
||||
|
||||
const commit = () => {
|
||||
if (selection?.from && selection?.to) {
|
||||
onChange(toBoundary(selection.from, selection.to));
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handlePreset = (preset: DateRangePreset) => {
|
||||
const { from, to } = preset.range();
|
||||
setSelection({ from, to });
|
||||
// Presets are a complete selection — commit immediately.
|
||||
onChange(toBoundary(from, to));
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCalendarSelect = (range: DayPickerDateRange | undefined) => {
|
||||
if (!range) return;
|
||||
setSelection(range);
|
||||
};
|
||||
|
||||
// Sync local selection when the popover opens so it reflects the
|
||||
// latest committed value. Reverse the boundary normalization so
|
||||
// the calendar highlights the correct inclusive dates.
|
||||
const handleOpenChange = (next: boolean) => {
|
||||
if (next) {
|
||||
setSelection(fromBoundary(value));
|
||||
}
|
||||
setOpen(next);
|
||||
};
|
||||
|
||||
// Compare in the same coordinate space (raw calendar dates) so
|
||||
// re-selecting the identical range doesn't enable Apply.
|
||||
const committed = fromBoundary(value);
|
||||
const canApply =
|
||||
selection?.from &&
|
||||
selection?.to &&
|
||||
(selection.from.getTime() !== committed.from?.getTime() ||
|
||||
selection.to.getTime() !== committed.to?.getTime());
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<CalendarIcon className="size-4 text-content-secondary" />
|
||||
<span>{dayjs(value.startDate).format("MMM D, YYYY")}</span>
|
||||
<MoveRightIcon className="size-3.5 text-content-secondary" />
|
||||
<span>{dayjs(value.endDate).format("MMM D, YYYY")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0 overflow-x-hidden overflow-y-auto"
|
||||
align="end"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex">
|
||||
{/* Presets sidebar */}
|
||||
<div className="flex flex-col border-r border-border-default p-2 text-sm">
|
||||
{presets.map((preset) => (
|
||||
<button
|
||||
key={preset.label}
|
||||
type="button"
|
||||
onClick={() => handlePreset(preset)}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-md border-none outline-none bg-transparent px-3 py-1.5 text-left text-sm",
|
||||
"text-content-secondary hover:bg-surface-secondary hover:text-content-primary",
|
||||
"focus-visible:ring-2 focus-visible:ring-content-link",
|
||||
"transition-colors whitespace-nowrap",
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar + footer */}
|
||||
<div className="flex flex-col">
|
||||
{/* Selected range display */}
|
||||
<div className="flex items-center gap-2 border-b border-border-default px-4 py-2 text-sm">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md px-2 py-1 tabular-nums",
|
||||
selection?.from
|
||||
? "bg-surface-secondary text-content-primary"
|
||||
: "text-content-secondary",
|
||||
)}
|
||||
>
|
||||
{selection?.from
|
||||
? dayjs(selection.from).format("MMM D, YYYY")
|
||||
: "Start date"}
|
||||
</span>
|
||||
<MoveRightIcon className="size-3.5 text-content-secondary" />
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md px-2 py-1 tabular-nums",
|
||||
selection?.to
|
||||
? "bg-surface-secondary text-content-primary"
|
||||
: "text-content-secondary",
|
||||
)}
|
||||
>
|
||||
{selection?.to
|
||||
? dayjs(selection.to).format("MMM D, YYYY")
|
||||
: "End date"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Two-month calendar */}
|
||||
<div className="p-2">
|
||||
<Calendar
|
||||
mode="range"
|
||||
selected={selection}
|
||||
onSelect={handleCalendarSelect}
|
||||
numberOfMonths={2}
|
||||
disabled={{ after: new Date() }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Apply footer */}
|
||||
<div className="flex items-center justify-end gap-2 border-t border-border-default px-4 py-2">
|
||||
<Button variant="subtle" size="sm" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={commit} disabled={!canApply}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,238 +0,0 @@
|
||||
import "react-date-range/dist/styles.css";
|
||||
import "react-date-range/dist/theme/default.css";
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import dayjs from "dayjs";
|
||||
import { MoveRightIcon } from "lucide-react";
|
||||
import { type ComponentProps, type FC, useRef, useState } from "react";
|
||||
import { createStaticRanges, DateRangePicker } from "react-date-range";
|
||||
|
||||
// The type definition from @types is wrong
|
||||
declare module "react-date-range" {
|
||||
export function createStaticRanges(
|
||||
ranges: Omit<StaticRange, "isSelected">[],
|
||||
): StaticRange[];
|
||||
}
|
||||
|
||||
export type DateRangeValue = {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
};
|
||||
|
||||
type RangesState = NonNullable<
|
||||
ComponentProps<typeof DateRangePicker>["ranges"]
|
||||
>;
|
||||
|
||||
interface DateRangeProps {
|
||||
value: DateRangeValue;
|
||||
onChange: (value: DateRangeValue) => void;
|
||||
}
|
||||
|
||||
export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
||||
const selectionStatusRef = useRef<"idle" | "selecting">("idle");
|
||||
const selectionSourceRef = useRef<"preset" | null>(null);
|
||||
const [ranges, setRanges] = useState<RangesState>([
|
||||
{
|
||||
...value,
|
||||
key: "selection",
|
||||
},
|
||||
]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const applyRangeSelection = (range: RangesState[number]) => {
|
||||
selectionStatusRef.current = "idle";
|
||||
const startDate = range.startDate as Date;
|
||||
const endDate = range.endDate as Date;
|
||||
const now = new Date();
|
||||
onChange({
|
||||
startDate: dayjs(startDate).startOf("day").toDate(),
|
||||
endDate: dayjs(endDate).isSame(dayjs(), "day")
|
||||
? dayjs(now).startOf("hour").add(1, "hour").toDate()
|
||||
: dayjs(endDate).startOf("day").add(1, "day").toDate(),
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<span>{dayjs(value.startDate).format("MMM D, YYYY")}</span>
|
||||
<MoveRightIcon />
|
||||
<span>{dayjs(value.endDate).format("MMM D, YYYY")}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0 overflow-x-hidden">
|
||||
<div
|
||||
onClickCapture={(event) => {
|
||||
selectionSourceRef.current =
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.closest(".rdrStaticRange")
|
||||
? "preset"
|
||||
: null;
|
||||
}}
|
||||
>
|
||||
<DateRangePicker
|
||||
css={styles.wrapper}
|
||||
onChange={(item) => {
|
||||
const range = item.selection;
|
||||
const isPresetSelection = selectionSourceRef.current === "preset";
|
||||
selectionSourceRef.current = null;
|
||||
setRanges([range]);
|
||||
|
||||
// When it is the first calendar selection, we don't want to
|
||||
// close the popover. Presets already provide a complete range,
|
||||
// so apply them immediately.
|
||||
if (selectionStatusRef.current === "idle" && !isPresetSelection) {
|
||||
selectionStatusRef.current = "selecting";
|
||||
return;
|
||||
}
|
||||
|
||||
applyRangeSelection(range);
|
||||
}}
|
||||
moveRangeOnFirstSelection={false}
|
||||
months={2}
|
||||
ranges={ranges}
|
||||
maxDate={new Date()}
|
||||
direction="horizontal"
|
||||
staticRanges={createStaticRanges([
|
||||
{
|
||||
label: "Today",
|
||||
range: () => ({
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Yesterday",
|
||||
range: () => ({
|
||||
startDate: dayjs().subtract(1, "day").toDate(),
|
||||
endDate: dayjs().subtract(1, "day").toDate(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
range: () => ({
|
||||
startDate: dayjs().subtract(6, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
range: () => ({
|
||||
startDate: dayjs().subtract(13, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
range: () => ({
|
||||
startDate: dayjs().subtract(29, "day").toDate(),
|
||||
endDate: new Date(),
|
||||
}),
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
wrapper: (theme) => ({
|
||||
"& .rdrDefinedRangesWrapper": {
|
||||
background: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
|
||||
"& .rdrStaticRange": {
|
||||
background: theme.palette.background.paper,
|
||||
border: 0,
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
"&:is(:hover, :focus) .rdrStaticRangeLabel": {
|
||||
background: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"&.rdrStaticRangeSelected": {
|
||||
color: `${theme.palette.text.primary} !important`,
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInputRanges": {
|
||||
display: "none",
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayWrapper": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
"& .rdrCalendarWrapper": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
"& .rdrDateDisplayItem": {
|
||||
background: "transparent",
|
||||
borderColor: theme.palette.divider,
|
||||
|
||||
"& input": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"&.rdrDateDisplayItemActive": {
|
||||
borderColor: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
"& input": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrMonthPicker select, & .rdrYearPicker select": {
|
||||
color: theme.palette.text.primary,
|
||||
appearance: "auto",
|
||||
background: "transparent",
|
||||
},
|
||||
|
||||
"& .rdrMonthName, & .rdrWeekDay": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"& .rdrDayPassive .rdrDayNumber span": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
"& .rdrDayToday .rdrDayNumber span": {
|
||||
fontWeight: 900,
|
||||
|
||||
"&:after": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
|
||||
"& .rdrInRange, & .rdrEndEdge, & .rdrStartEdge": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
|
||||
"& .rdrDayDisabled": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
"& .rdrDayNumber span": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
@@ -41,6 +41,10 @@ import {
|
||||
SquareArrowOutUpRightIcon,
|
||||
} from "lucide-react";
|
||||
import { RequirePermission } from "modules/permissions/RequirePermission";
|
||||
import {
|
||||
DateRangePicker as DailyPicker,
|
||||
type DateRangeValue,
|
||||
} from "pages/AgentsPage/components/DateRangePicker/DateRangePicker";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import {
|
||||
type FC,
|
||||
@@ -62,7 +66,6 @@ import {
|
||||
subtractTime,
|
||||
} from "utils/time";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { DateRange as DailyPicker, type DateRangeValue } from "./DateRange";
|
||||
import { type InsightsInterval, IntervalMenu } from "./IntervalMenu";
|
||||
import { lastWeeks } from "./utils";
|
||||
import { numberOfWeeksOptions, WeekPicker } from "./WeekPicker";
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "components/DropdownMenu/DropdownMenu";
|
||||
import dayjs from "dayjs";
|
||||
import type { DateRangeValue } from "pages/AgentsPage/components/DateRangePicker/DateRangePicker";
|
||||
import type { FC } from "react";
|
||||
import type { DateRangeValue } from "./DateRange";
|
||||
import { lastWeeks } from "./utils";
|
||||
|
||||
// There is no point in showing the period > 6 months. We prune stats older than
|
||||
|
||||
Reference in New Issue
Block a user