import type { Meta, StoryObj } from "@storybook/nextjs-vite"; import { useState } 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 React.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
)}
); }, };