import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { useState } from 'react'; import type { ComponentType } from 'react'; import { DateFilterMenu, type DateFilterResult, DatePicker, DatePickerInput, DatePickerInputSimple, type DatePickerBaseProps, type DateRange, } from '../components/ui/date-picker'; import type { SortDirection } from '../components/ui/sort-column-item'; // Using a simplified type for Storybook since DatePicker uses discriminated union type DatePickerStoryProps = DatePickerBaseProps & { mode?: 'single' | 'range'; }; const meta: Meta = { title: 'UI/DatePicker', component: DatePicker as ComponentType, parameters: { layout: 'centered', docs: { description: { component: 'DatePicker component with single date and date range selection modes. Follows ODS design system with dark theme styling.', }, }, }, tags: ['autodocs'], argTypes: { mode: { control: 'select', options: ['single', 'range'], description: 'Selection mode: single date or date range', }, placeholder: { control: 'text', description: 'Placeholder text when no date is selected', }, disabled: { control: 'boolean', description: 'Whether the picker is disabled', }, numberOfMonths: { control: 'select', options: [1, 2], description: 'Number of months to display in the calendar', }, }, }; export default meta; type Story = StoryObj; /** * Single date picker - default mode. */ export const Single: Story = { render: function Render() { const [date, setDate] = useState(); return ; }, }; /** * Single date picker with a pre-selected date. */ export const SingleWithValue: Story = { render: function Render() { const [date, setDate] = useState(new Date()); return ; }, }; /** * Date range picker - for selecting a period. */ export const Range: Story = { render: function Render() { const [range, setRange] = useState(); return ; }, }; /** * Date range picker with pre-selected range. */ export const RangeWithValue: Story = { render: function Render() { const today = new Date(); const nextWeek = new Date(today); nextWeek.setDate(today.getDate() + 7); const [range, setRange] = useState({ from: today, to: nextWeek, }); return ; }, }; /** * Date range picker with two months displayed. */ export const RangeTwoMonths: Story = { render: function Render() { const [range, setRange] = useState(); return ( ); }, decorators: [ Story => (
), ], }; /** * Disabled date picker. */ export const Disabled: Story = { render: function Render() { return ; }, }; /** * Date picker with label. */ export const WithLabel: Story = { render: function Render() { const [date, setDate] = useState(); return ; }, }; /** * Date picker with label and error. */ export const WithLabelAndError: Story = { render: function Render() { return ; }, }; /** * Date range picker with label. */ export const RangeWithLabel: Story = { render: function Render() { const [range, setRange] = useState(); return ; }, }; /** * Date picker with custom date format. */ export const CustomFormat: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDate = (d: Date) => d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', }); return ( ); }, }; /** * Date picker with min/max date constraints. */ export const WithConstraints: Story = { render: function Render() { const [date, setDate] = useState(); const today = new Date(); const maxDate = new Date(); maxDate.setDate(today.getDate() + 30); return ( ); }, }; // ============================================================================ // DatePickerInput Stories // ============================================================================ /** * DatePickerInput - styled as input field with calendar icon. */ export const InputStyle: Story = { render: function Render() { const [date, setDate] = useState(); return ; }, }; /** * DatePickerInput with label. */ export const InputWithLabel: Story = { render: function Render() { const [date, setDate] = useState(); return (
); }, }; /** * DatePickerInput with label and error. */ export const InputWithLabelAndError: Story = { render: function Render() { return (
); }, }; /** * DatePickerInput with time selector - empty initial state. */ export const InputWithTimeEmpty: Story = { render: function Render() { const [date, setDate] = useState(); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * DatePickerInput with time selector (separate hour/minute/period selects). */ export const InputWithTime: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * DatePickerInput with 24-hour time format. */ export const InputWithTime24Hour: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * All DatePicker variants displayed together. */ export const AllVariants: Story = { render: function Render() { const [singleDate, setSingleDate] = useState(); const [rangeDate, setRangeDate] = useState(); const [inputDate, setInputDate] = useState(new Date()); const formatSingleDate = (date: Date | undefined) => { if (!date) return 'No date selected'; return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); }; const formatRangeDate = (range: DateRange | undefined) => { if (!range?.from) return 'No range selected'; const fromStr = range.from.toLocaleDateString('en-US', { month: 'short', day: 'numeric', }); if (!range.to) return `${fromStr} - ...`; const toStr = range.to.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); return `${fromStr} - ${toStr}`; }; const formatDateTime = () => { if (!inputDate) return 'No date selected'; const dateStr = inputDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = inputDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatSingleDate(singleDate)}
Selected: {formatRangeDate(rangeDate)}
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; // ============================================================================ // DatePickerInputSimple Stories // ============================================================================ /** * DatePickerInputSimple - simplified version with single time selector dropdown. */ export const SimpleInputWithTime: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * DatePickerInputSimple with 15-minute intervals. */ export const SimpleInputWithTime15Min: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * DatePickerInputSimple with 24-hour format. */ export const SimpleInputWithTime24Hour: Story = { render: function Render() { const [date, setDate] = useState(new Date()); const formatDateTime = () => { if (!date) return 'No date selected'; const dateStr = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }); const timeStr = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, }); return `${dateStr} at ${timeStr}`; }; return (
Selected: {formatDateTime()}
); }, decorators: [ Story => (
), ], }; /** * DatePickerInputSimple without time selector. */ export const SimpleInputDateOnly: Story = { render: function Render() { const [date, setDate] = useState(); return (
Selected:{' '} {date ? date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', }) : 'No date selected'}
); }, decorators: [ Story => (
), ], }; // ============================================================================ // DateFilterMenu Stories (sort + calendar filter popover from Figma) // ============================================================================ const fmtSort = (sort: SortDirection) => (sort === 'asc' ? 'Ascending' : 'Descending'); const fmtDay = (d: Date) => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); /** * DateFilterMenu — calendar-icon trigger opens a popover with a sort selector, * a date-range calendar, and Close/Reset + Apply actions. Matches the Figma * `filter-menu`. The sort and date selection are drafted and committed on * Apply. With a selection present, Close becomes Reset, which clears and * commits the empty selection. */ export const FilterMenuRange: Story = { render: function Render() { const [applied, setApplied] = useState(null); const summary = () => { if (!applied) return 'Nothing applied yet'; const { sort, range } = applied; const r = range?.from ? range.to ? `${fmtDay(range.from)} - ${fmtDay(range.to)}` : fmtDay(range.from) : 'all dates'; return `Sort ${fmtSort(sort)} · ${r}`; }; return (
Last activity
Applied: {summary()}
); }, }; /** * DateFilterMenu in single-date mode. */ export const FilterMenuSingle: Story = { render: function Render() { const [applied, setApplied] = useState(null); return (
Date & time
Applied:{' '} {applied ? `Sort ${fmtSort(applied.sort)} · ${applied.date ? fmtDay(applied.date) : 'all dates'}` : 'Nothing applied yet'}
); }, }; /** * DateFilterMenu wired to filter + sort a small table — mirrors the intended * usage in table columns (organizations last-activity, scripts schedules, * logs). Apply re-filters and re-sorts the rows by date. */ export const FilterMenuWithTable: Story = { render: function Render() { type Row = { id: string; activity: Date }; const rows: Row[] = [ { id: 'Acme Corp', activity: new Date(2026, 4, 2) }, { id: 'Globex', activity: new Date(2026, 4, 6) }, { id: 'Initech', activity: new Date(2026, 4, 11) }, { id: 'Umbrella', activity: new Date(2026, 4, 18) }, { id: 'Stark Ind.', activity: new Date(2026, 4, 24) }, ]; const [filter, setFilter] = useState({ sort: 'desc', range: undefined, }); const visible = rows .filter(row => { const { from, to } = filter.range ?? {}; if (from && row.activity < from) return false; if (to && row.activity > to) return false; return true; }) .sort((a, b) => filter.sort === 'asc' ? a.activity.getTime() - b.activity.getTime() : b.activity.getTime() - a.activity.getTime(), ); return (
Organizations
{visible.map(row => (
{row.id} {fmtDay(row.activity)}
))} {visible.length === 0 && (
No results in range
)}
); }, };