Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
622 changes: 622 additions & 0 deletions apps/eclipse/content/design-system/components/datepicker.mdx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/eclipse/content/design-system/components/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"chart",
"checkbox",
"codeblock",
"datepicker",
"dialog",
"dropdownmenu",
"empty",
Expand Down
106 changes: 106 additions & 0 deletions apps/eclipse/src/components/date-picker-examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client";

import { useState } from "react";
import {
DatePickerSingle,
DatePickerRange,
createDateRangePresets,
} from "@prisma/eclipse";
import type { DateRange } from "@prisma/eclipse";

export function DatePickerSingleExample() {
const [date, setDate] = useState<Date>();

return (
<DatePickerSingle
date={date}
onDateChange={setDate}
placeholder="Select a date"
dateFormat="dd/MM/yyyy"
/>
);
}

export function DatePickerRangeExample() {
const [dateRange, setDateRange] = useState<DateRange>();

return (
<DatePickerRange
dateRange={dateRange}
onDateRangeChange={setDateRange}
placeholder="Pick a date range"
/>
);
}

export function DatePickerRangeWithPresetsExample() {
const [dateRange, setDateRange] = useState<DateRange>();
const presets = createDateRangePresets();

return (
<DatePickerRange
dateRange={dateRange}
onDateRangeChange={setDateRange}
presets={presets}
placeholder="Pick a date range with presets"
/>
);
}

export function DatePickerErrorExample() {
return (
<div className="space-y-2">
<DatePickerSingle placeholder="Error state example" isErrored={true} />
<p className="text-sm text-foreground-error">Date is required</p>
</div>
);
}

export function DatePickerDisabledExample() {
return (
<DatePickerSingle placeholder="Disabled state example" disabledBtn={true} />
);
}

export function DatePickerWithValidationExample() {
const [date, setDate] = useState<Date>();
const [submitted, setSubmitted] = useState(false);

const hasError = submitted && !date;

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
if (date) {
alert(`Date selected: ${date.toLocaleDateString()}`);
}
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Event Date *</label>
<DatePickerSingle
date={date}
onDateChange={(newDate) => {
setDate(newDate);
if (newDate) setSubmitted(false);
}}
placeholder="Select event date"
isErrored={hasError}
/>
{hasError && (
<p className="text-sm text-foreground-error">
Please select an event date
</p>
)}
</div>
<button
type="submit"
className="px-4 py-2 bg-background-ppg text-foreground-ppg rounded-md hover:bg-background-ppg/90"
>
Submit
</button>
</form>
);
}
2 changes: 2 additions & 0 deletions packages/eclipse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"@radix-ui/react-tooltip": "catalog:",
"@tailwindcss/postcss": "catalog:",
"class-variance-authority": "catalog:",
"date-fns": "^4.1.0",
"lucide-react": "catalog:",
"react-day-picker": "^9.14.0",
"recharts": "catalog:",
"tailwind-merge": "catalog:",
"tw-animate-css": "catalog:"
Expand Down
242 changes: 242 additions & 0 deletions packages/eclipse/src/components/date-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"use client";

import * as React from "react";
import { format } from "date-fns";
import type { DateRange, Matcher } from "react-day-picker";

import { cn } from "../lib/cn";
import { Button } from "./button";
import { Calendar } from "./ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";

// Re-export types for consumers
export type { DateRange, Matcher } from "react-day-picker";

export interface DatePickerProps {
/**
* The selected date (for single date picker)
*/
date?: Date;
/**
* Callback when date changes (for single date picker)
*/
onDateChange?: (date: Date | undefined) => void;
/**
* The selected date range (for range picker)
*/
dateRange?: DateRange;
/**
* Callback when date range changes (for range picker)
*/
onDateRangeChange?: (range: DateRange | undefined) => void;
/**
* Placeholder text when no date is selected
*/
placeholder?: string;
/**
* Mode: 'single' or 'range'
*/
mode?: "single" | "range";
/**
* Preset date ranges (for range picker)
*/
presets?: Array<{
label: string;
dateRange: DateRange;
}>;
/**
* Disabled dates
*/
disabled?: Matcher | Matcher[];
/**
* Custom className for the trigger button
*/
className?: string;
/**
* Align popover content
*/
align?: "start" | "center" | "end";
/**
* Whether the date picker is in an error state
*/
isErrored?: boolean;
/**
* Whether the trigger button is disabled
*/
disabledBtn?: boolean;
/**
* Date format string for displaying dates (date-fns format)
* @default "PPP" for single mode (e.g., "February 17th, 2026")
* @default "LLL dd, y" for range mode (e.g., "Feb 17, 2026")
* @example "dd/MM/yyyy" → "17/02/2026"
* @example "MM/dd/yyyy" → "02/17/2026"
* @example "yyyy-MM-dd" → "2026-02-17"
*/
dateFormat?: string;
Comment thread
carlagn marked this conversation as resolved.
}

export function DatePicker({
date,
onDateChange,
dateRange,
onDateRangeChange,
placeholder,
mode = "single",
presets,
disabled,
className,
align = "start",
isErrored = false,
disabledBtn = false,
dateFormat,
}: DatePickerProps) {
const [open, setOpen] = React.useState(false);

// Single date picker
if (mode === "single") {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="lg"
className={cn(
"w-full p-1.5 text-left font-normal bg-background-default border-stroke-neutral font-mono text-foreground-neutral",
!date && "text-foreground-neutral-weak",
isErrored && "border-stroke-error text-foreground-error",
disabledBtn &&
"cursor-not-allowed text-foreground-neutral-weaker bg-background-neutral-weak",
className,
)}
type="button"
>
Comment on lines +100 to +112
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

disabledBtn is styling-only; the picker is still interactive.

On Line 107 and Line 154, the component looks disabled but neither trigger Button is actually disabled, so users can still open/select dates.

Suggested fix (apply to both single and range trigger buttons)
- <Popover open={open} onOpenChange={setOpen}>
+ <Popover
+   open={open}
+   onOpenChange={(nextOpen) => {
+     if (!disabledBtn) setOpen(nextOpen);
+   }}
+ >
...
- <Button
+ <Button
+   disabled={disabledBtn}
    variant="default"
    size="lg"

Also applies to: 147-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/eclipse/src/components/date-picker.tsx` around lines 100 - 112, The
visual "disabled" state (disabledBtn) is only applied as styling so the
DatePicker triggers remain interactive; update the trigger Button elements (the
single-date trigger and the range triggers identified by the Button components
inside date-picker.tsx) to actually disable interaction when disabledBtn is true
by adding the disabled prop (disabled={disabledBtn}) and also include
aria-disabled={disabledBtn} for accessibility; keep the existing className
conditional logic (isErrored, className) but ensure the Button elements
reference disabledBtn to prevent opening/selection when disabled.

<i
className={cn(
"text-foreground-neutral-weak fa-duotone fa-calendar-range flex h-full items-center text-md before:inset-y-0 -mt-0.5",
(isErrored || disabledBtn) && "text-inherit",
)}
/>
{date ? (
format(date, dateFormat || "P")
) : (
<span>{placeholder || "Pick a date"}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align={align}>
<Calendar
mode="single"
selected={date}
onSelect={(newDate) => {
onDateChange?.(newDate);
setOpen(false);
}}
disabled={disabled}
initialFocus
/>
Comment thread
carlagn marked this conversation as resolved.
</PopoverContent>
</Popover>
);
}

// Range date picker
return (
<div className="flex flex-col gap-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="lg"
className={cn(
"w-full p-1.5 justify-start text-left font-normal bg-background-default border-stroke-neutral font-mono text-foreground-neutral",
!dateRange && "text-foreground-neutral-weak",
isErrored && "border-stroke-error text-foreground-error",
disabledBtn &&
"cursor-not-allowed text-foreground-neutral-weaker bg-background-neutral-weak",
className,
)}
type="button"
>
<i
className={cn(
"text-foreground-neutral-weak fa-duotone fa-calendar-range flex h-full items-center before:inset-y-0 -mt-0.5 text-md",
(isErrored || disabledBtn) && "text-inherit",
)}
/>
{dateRange?.from ? (
dateRange.to ? (
<>
{format(dateRange.from, dateFormat || "dd/MM/yyyy")} -{" "}
{format(dateRange.to, dateFormat || "dd/MM/yyyy")}
</>
) : (
format(dateRange.from, dateFormat || "dd/MM/yyyy")
)
) : (
<span>{placeholder || "Pick a date range"}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align={align}>
<div className="flex">
{presets && presets.length > 0 && (
<div className="flex flex-col gap-1 border-r p-3">
<div className="text-xs font-medium text-muted-foreground mb-1">
Presets
</div>
{presets.map((preset, index) => (
<Button
key={index}
variant="default-weaker"
size="lg"
className="justify-start"
type="button"
onClick={() => {
onDateRangeChange?.(preset.dateRange);
setOpen(false);
}}
>
{preset.label}
</Button>
))}
</div>
)}
<Calendar
mode="range"
selected={dateRange}
onSelect={onDateRangeChange}
disabled={disabled}
numberOfMonths={2}
initialFocus
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}

// Convenience exports for specific use cases
export function DatePickerSingle(
props: Omit<
DatePickerProps,
"mode" | "dateRange" | "onDateRangeChange" | "presets"
>,
) {
return <DatePicker {...props} mode="single" />;
}

export function DatePickerRange(
props: Omit<DatePickerProps, "mode" | "date" | "onDateChange">,
) {
return <DatePicker {...props} mode="range" />;
}

// Re-export helper functions from lib
export {
createDateRangePresets,
createDateRangePreset,
getLastNDays,
getCurrentMonth,
getPreviousMonth,
} from "../lib/date-presets";
Loading
Loading