diff --git a/apps/eclipse/content/design-system/components/datepicker.mdx b/apps/eclipse/content/design-system/components/datepicker.mdx new file mode 100644 index 0000000000..2a36609eaa --- /dev/null +++ b/apps/eclipse/content/design-system/components/datepicker.mdx @@ -0,0 +1,622 @@ +--- +title: Date Picker +description: A flexible date picker component with support for single date selection, date ranges, and preset options. +--- + +import { + DatePickerSingleExample, + DatePickerRangeExample, + DatePickerRangeWithPresetsExample, + DatePickerErrorExample, + DatePickerDisabledExample, + DatePickerWithValidationExample, +} from "../../../src/components/date-picker-examples"; + +### Usage + +**Single Date Picker** + +```tsx +import { DatePickerSingle } from "@prisma-docs/eclipse"; +import { useState } from "react"; + +export function SingleDateExample() { + const [date, setDate] = useState(); + + return ( + + ); +} +``` + +**Live Example:** + +
+ +
+ +**Date Range Picker** + +```tsx +import { DatePickerRange } from "@prisma-docs/eclipse"; +import { useState } from "react"; +import type { DateRange } from "react-day-picker"; + +export function RangeDateExample() { + const [dateRange, setDateRange] = useState(); + + return ( + + ); +} +``` + +**Live Example:** + +
+ +
+ +**Date Range Picker with Presets** + +```tsx +import { + DatePickerRange, + createDateRangePresets, +} from "@prisma-docs/eclipse"; +import { useState } from "react"; +import type { DateRange } from "react-day-picker"; + +export function RangeWithPresets() { + const [dateRange, setDateRange] = useState(); + const presets = createDateRangePresets(); + + return ( + + ); +} +``` + +**Live Example:** + +
+ +
+ +**Unified Component** + +The unified `DatePicker` component supports both modes: + +```tsx +import { DatePicker } from "@prisma-docs/eclipse"; +import { useState } from "react"; + +export function UnifiedExample() { + const [date, setDate] = useState(); + + return ( + + ); +} +``` + +### DatePicker Props + +#### Common Props + +- `placeholder` - Placeholder text when no date is selected (optional) +- `disabled` - Disabled dates (can be a function, date, or array) (optional) +- `className` - Custom className for the trigger button (optional) +- `align` - Align popover content: `"start"`, `"center"`, or `"end"` (default: `"start"`) +- `isErrored` - Whether the date picker is in an error state (optional, default: `false`) +- `disabledBtn` - Whether the trigger button is disabled (optional, default: `false`) +- `dateFormat` - Date format string for displaying dates using date-fns format tokens (optional) + +#### Single Date Mode Props + +- `mode` - Set to `"single"` for single date selection +- `date` - The selected date (optional) +- `onDateChange` - Callback when date changes: `(date: Date | undefined) => void` (optional) + +#### Range Date Mode Props + +- `mode` - Set to `"range"` for date range selection +- `dateRange` - The selected date range (optional) +- `onDateRangeChange` - Callback when date range changes: `(range: DateRange | undefined) => void` (optional) +- `presets` - Array of preset date ranges (optional) + +### Component Variants + +#### DatePickerSingle + +Convenience component that automatically sets `mode="single"`: + +```tsx + +``` + +#### DatePickerRange + +Convenience component that automatically sets `mode="range"`: + +```tsx + +``` + +### Preset Helper + +Use `createDateRangePresets()` to generate common date range presets: + +```tsx +import { createDateRangePresets } from "@prisma-docs/eclipse"; + +const presets = createDateRangePresets(); +// Returns: Today, Last 7 days, Last 14 days, Last 30 days, +// Last 90 days, This month, Last month +``` + +**Custom Presets** + +```tsx +import { startOfWeek } from "date-fns"; + +const customPresets = [ + { + label: "Yesterday", + dateRange: { + from: new Date(Date.now() - 24 * 60 * 60 * 1000), + to: new Date(Date.now() - 24 * 60 * 60 * 1000), + }, + }, + { + label: "This Week", + dateRange: { + from: startOfWeek(new Date()), + to: new Date(), + }, + }, +]; + + +``` + +### Features + +- ✅ Single date selection +- ✅ Date range selection +- ✅ Preset date ranges +- ✅ Custom disabled dates +- ✅ Flexible date formatting with date-fns +- ✅ Keyboard navigation +- ✅ Accessible with proper ARIA attributes +- ✅ Eclipse design system integration +- ✅ Responsive popover positioning + +### Common Use Cases + +**Event Scheduling** + +```tsx + { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + }} +/> +``` + +**Report Date Range** + +```tsx + +``` + +**Booking System** + +```tsx + { + const day = date.getDay(); + return day === 0 || day === 6; // Disable weekends + }} + placeholder="Select booking dates" +/> +``` + +**Analytics Dashboard** + +```tsx +const analyticsPresets = [ + { label: "Last 7 days", dateRange: { from: sevenDaysAgo, to: today } }, + { label: "Last 30 days", dateRange: { from: thirtyDaysAgo, to: today } }, + { label: "This Quarter", dateRange: { from: quarterStart, to: today } }, +]; + + +``` + +### Disabling Dates + +**Disable Past Dates** + +```tsx + { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + }} +/> +``` + +**Disable Future Dates** + +```tsx + { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date > today; + }} +/> +``` + +**Disable Specific Dates** + +```tsx +const disabledDates = [ + new Date(2024, 11, 25), // Christmas + new Date(2025, 0, 1), // New Year +]; + + +``` + +**Disable Date Ranges** + +```tsx + +``` + +**Disable Days of Week** + +```tsx + { + const day = date.getDay(); + return day === 0 || day === 6; // Disable weekends + }} +/> +``` + +### Error State + +Use the `isErrored` prop to indicate validation errors: + +```tsx +import { DatePickerSingle } from "@prisma-docs/eclipse"; +import { useState } from "react"; + +export function FormWithValidation() { + const [date, setDate] = useState(); + const [submitted, setSubmitted] = useState(false); + + const hasError = submitted && !date; + + return ( +
+ + {hasError && ( +

+ Date is required +

+ )} +
+ ); +} +``` + +**Live Example (Static Error State):** + +
+ +
+ +**Live Example (With Validation):** + +
+ +
+ +The error state adds a red ring (`ring-2 ring-stroke-error`) to the button and changes the text and icon color to `text-foreground-error` to indicate the validation error. + +### Disabled State + +Use the `disabledBtn` prop to disable the entire date picker button: + +```tsx + +``` + +**Live Example:** + +
+ +
+ +**Use Cases:** +- Disable based on permissions +- Disable while data is loading +- Disable when form is submitting +- Conditional disabling based on other form fields + +```tsx +function ConditionalDatePicker() { + const [isSubscribed, setIsSubscribed] = useState(false); + const [date, setDate] = useState(); + + return ( +
+ + + +
+ ); +} +``` + +### Formatting + +The component uses `date-fns` for date formatting. You can customize the format using the `dateFormat` prop: + +**Default Formats:** +- **Single date**: `PPP` format (e.g., "April 29, 2024") +- **Date range**: `LLL dd, y` format (e.g., "Apr 01, 2024 - Apr 30, 2024") + +**Custom Formats:** + +```tsx +// European format: dd/MM/yyyy + +// Output: "17/02/2026" + +// US format: MM/dd/yyyy + +// Output: "02/17/2026" + +// ISO format: yyyy-MM-dd + +// Output: "2026-02-17" + +// Custom verbose format + +// Output: "Tuesday, February 17th, 2026 - Friday, February 20th, 2026" +``` + +**Common date-fns Format Tokens:** +- `dd` - Day of month (01-31) +- `MM` - Month (01-12) +- `yyyy` - Full year (2026) +- `yy` - 2-digit year (26) +- `MMM` - Short month name (Feb) +- `MMMM` - Full month name (February) +- `do` - Day with ordinal (17th) +- `EEEE` - Full day name (Tuesday) +- `PPP` - Long localized date (February 17th, 2026) +- `P` - Short localized date (02/17/2026) + +### Best Practices + +- Use **single date picker** for events, deadlines, or appointments +- Use **range picker** for reports, analytics, or booking periods +- Provide **presets** for common date ranges to improve UX +- **Disable irrelevant dates** (e.g., past dates for future bookings) +- Use clear **placeholder text** that indicates what the date is for +- Consider **default values** for better user experience +- Add **validation** to ensure date ranges make sense +- Show **clear labels** above date pickers in forms +- Use **consistent date formats** across your application +- Use `isErrored` prop with validation messages for better UX +- Use `disabledBtn` for conditional access or loading states +- Choose appropriate `dateFormat` based on your user's locale and preferences + +### Accessibility + +- Full keyboard navigation support +- ARIA labels and roles for screen readers +- Focus management within the calendar +- Escape key to close the popover +- Tab navigation between dates +- Enter/Space to select dates +- Arrow keys to navigate calendar days +- High contrast colors for readability +- Clear visual focus indicators + +### Design Tokens + +The component uses Eclipse design system tokens: + +- Button: Standard button variants and sizes +- Popover: `bg-popover`, `text-popover-foreground` +- Calendar: Eclipse calendar component styling +- Borders: `border-stroke-neutral` +- Text: `text-muted-foreground` for placeholder +- Icons: Lucide React `CalendarIcon` + +### TypeScript Support + +The component is fully typed with TypeScript: + +```tsx +import type { DateRange, Matcher } from "react-day-picker"; +import type { DatePickerProps } from "@prisma-docs/eclipse"; + +const props: DatePickerProps = { + mode: "range", + dateRange: { from: new Date(), to: new Date() }, + onDateRangeChange: (range) => console.log(range), + presets: [ + { label: "Last 7 days", dateRange: { from: new Date(), to: new Date() } }, + ], +}; +``` + +### Integration with Forms + +**With React Hook Form** + +```tsx +import { useForm, Controller } from "react-hook-form"; +import { DatePickerSingle } from "@prisma-docs/eclipse"; + +function MyForm() { + const { control, handleSubmit, formState: { errors } } = useForm(); + + const onSubmit = (data: Record) => { + console.log("Form submitted:", data); + }; + + return ( +
+ ( +
+ + {errors.eventDate && ( +

+ {errors.eventDate.message} +

+ )} +
+ )} + /> + + ); +} +``` + +**With Native State** + +```tsx +function MyForm() { + const [startDate, setStartDate] = useState(); + const [endDate, setEndDate] = useState(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log({ startDate, endDate }); + }; + + return ( +
+
+ + + +
+
+ ); +} +``` diff --git a/apps/eclipse/content/design-system/components/meta.json b/apps/eclipse/content/design-system/components/meta.json index 5d0d142b60..289fb401dd 100644 --- a/apps/eclipse/content/design-system/components/meta.json +++ b/apps/eclipse/content/design-system/components/meta.json @@ -15,6 +15,7 @@ "chart", "checkbox", "codeblock", + "datepicker", "dialog", "dropdownmenu", "empty", diff --git a/apps/eclipse/src/components/date-picker-examples.tsx b/apps/eclipse/src/components/date-picker-examples.tsx new file mode 100644 index 0000000000..a190df6e81 --- /dev/null +++ b/apps/eclipse/src/components/date-picker-examples.tsx @@ -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(); + + return ( + + ); +} + +export function DatePickerRangeExample() { + const [dateRange, setDateRange] = useState(); + + return ( + + ); +} + +export function DatePickerRangeWithPresetsExample() { + const [dateRange, setDateRange] = useState(); + const presets = createDateRangePresets(); + + return ( + + ); +} + +export function DatePickerErrorExample() { + return ( +
+ +

Date is required

+
+ ); +} + +export function DatePickerDisabledExample() { + return ( + + ); +} + +export function DatePickerWithValidationExample() { + const [date, setDate] = useState(); + 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 ( +
+
+ + { + setDate(newDate); + if (newDate) setSubmitted(false); + }} + placeholder="Select event date" + isErrored={hasError} + /> + {hasError && ( +

+ Please select an event date +

+ )} +
+ +
+ ); +} diff --git a/packages/eclipse/package.json b/packages/eclipse/package.json index 0e941161a6..b414409be7 100644 --- a/packages/eclipse/package.json +++ b/packages/eclipse/package.json @@ -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:" diff --git a/packages/eclipse/src/components/date-picker.tsx b/packages/eclipse/src/components/date-picker.tsx new file mode 100644 index 0000000000..b5ebd822a9 --- /dev/null +++ b/packages/eclipse/src/components/date-picker.tsx @@ -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; +} + +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 ( + + + + + + { + onDateChange?.(newDate); + setOpen(false); + }} + disabled={disabled} + initialFocus + /> + + + ); + } + + // Range date picker + return ( +
+ + + + + +
+ {presets && presets.length > 0 && ( +
+
+ Presets +
+ {presets.map((preset, index) => ( + + ))} +
+ )} + +
+
+
+
+ ); +} + +// Convenience exports for specific use cases +export function DatePickerSingle( + props: Omit< + DatePickerProps, + "mode" | "dateRange" | "onDateRangeChange" | "presets" + >, +) { + return ; +} + +export function DatePickerRange( + props: Omit, +) { + return ; +} + +// Re-export helper functions from lib +export { + createDateRangePresets, + createDateRangePreset, + getLastNDays, + getCurrentMonth, + getPreviousMonth, +} from "../lib/date-presets"; diff --git a/packages/eclipse/src/components/fontawesome-script.tsx b/packages/eclipse/src/components/fontawesome-script.tsx new file mode 100644 index 0000000000..d9328e8714 --- /dev/null +++ b/packages/eclipse/src/components/fontawesome-script.tsx @@ -0,0 +1,37 @@ +"use client"; + +import Script from "next/script"; + +/** + * FontAwesome Script Component + * + * Loads the FontAwesome kit script required for eclipse design system icons. + * This component should be added to your app's layout to enable FontAwesome icons + * used throughout the eclipse components. + * + * @example + * ```tsx + * import { FontAwesomeScript } from "@prisma-docs/eclipse"; + * + * export default function RootLayout({ children }) { + * return ( + * + * + * {children} + * + * + * + * ); + * } + * ``` + */ +export function FontAwesomeScript() { + return ( +