From 3ddd1081869a6261915e400f52b332918f1221be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Mon, 23 Feb 2026 19:21:38 +0100 Subject: [PATCH] add datepicker, v0.1.9 --- package.json | 2 +- src/components/DatePicker.stories.tsx | 232 ++++++++++++++++++++++++++ src/components/DatePicker.tsx | 95 +++++++++++ src/index.ts | 2 + 4 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 src/components/DatePicker.stories.tsx create mode 100644 src/components/DatePicker.tsx diff --git a/package.json b/package.json index d7217f7..71a5710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@panic/web-ui", - "version": "0.1.8", + "version": "0.1.9", "license": "AGPL-3.0-only", "description": "Core components for panic.haus web applications", "type": "module", diff --git a/src/components/DatePicker.stories.tsx b/src/components/DatePicker.stories.tsx new file mode 100644 index 0000000..372975d --- /dev/null +++ b/src/components/DatePicker.stories.tsx @@ -0,0 +1,232 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CalendarDaysIcon } from '@heroicons/react/24/solid'; +import { DatePicker } from './DatePicker'; + +const meta = { + title: 'Components/DatePicker', + component: DatePicker, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Date selection field with InputField-compatible API, supporting date/time/datetime-local values, size/layout variants, and validation state.', + }, + }, + }, + argTypes: { + label: { + description: 'Label text shown above (stacked) or on the left (inline).', + control: 'text', + table: { type: { summary: 'string' } }, + }, + placeholder: { + description: 'Input placeholder text.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + type: { + description: 'Native date input type.', + options: ['date', 'datetime-local', 'time'], + control: 'inline-radio', + table: { type: { summary: "'date' | 'datetime-local' | 'time'" } }, + }, + size: { + description: 'Input size.', + options: ['sm', 'md', 'lg', 'full'], + control: 'inline-radio', + table: { type: { summary: "'sm' | 'md' | 'lg' | 'full'" } }, + }, + layout: { + description: 'Label/input layout mode.', + options: ['stacked', 'inline'], + control: 'inline-radio', + table: { type: { summary: "'stacked' | 'inline'" } }, + }, + value: { + description: 'Controlled input value.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + name: { + description: 'Native input `name` attribute.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + disabled: { + description: 'Disables the input.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + required: { + description: 'Sets the native HTML `required` attribute.', + control: 'boolean', + table: { type: { summary: 'boolean' } }, + }, + error: { + description: 'Validation message shown below the field.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + rightIcon: { + description: 'Optional trailing icon node.', + control: false, + table: { type: { summary: 'ReactNode' } }, + }, + className: { + description: 'Extra CSS classes for the outer wrapper.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + inputClassName: { + description: 'Extra CSS classes for the `` element.', + control: 'text', + table: { type: { summary: 'string' } }, + }, + onChange: { + description: 'Change handler callback.', + action: 'changed', + table: { type: { summary: 'ChangeEventHandler' } }, + }, + onBlur: { + description: 'Blur handler callback.', + control: false, + table: { type: { summary: 'FocusEventHandler' } }, + }, + inputRef: { + description: 'Ref forwarded to the native `` element.', + control: false, + table: { type: { summary: 'Ref' } }, + }, + }, + args: { + label: 'Schedule at', + type: 'datetime-local', + value: '', + size: 'md', + layout: 'stacked', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const DateOnly: Story = { + args: { + type: 'date', + label: 'Publish date', + }, + render: (args) => { + const [value, setValue] = useState('2031-05-20'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, +}; + +export const DateTime: Story = { + args: { + type: 'datetime-local', + label: 'Schedule at', + }, + render: (args) => { + const [value, setValue] = useState('2031-05-20T14:30'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, +}; + +export const TimeOnlyInline: Story = { + args: { + type: 'time', + label: 'Start time', + layout: 'inline', + size: 'sm', + rightIcon: , + }, + render: (args) => { + const [value, setValue] = useState('09:00'); + return ( + { + setValue(event.target.value); + args.onChange?.(event); + }} + /> + ); + }, +}; + +export const Error: Story = { + args: { + type: 'datetime-local', + label: 'Schedule at', + value: '', + error: 'Pick a valid future date and time', + }, +}; + +export const Disabled: Story = { + args: { + type: 'datetime-local', + label: 'Published at', + value: '2031-05-20T14:30', + disabled: true, + }, +}; + +export const SizeMatrix: Story = { + args: { + type: 'datetime-local', + label: 'Schedule at', + }, + render: (args) => { + const [value, setValue] = useState('2031-05-20T14:30'); + return ( +
+ setValue(event.target.value)} + /> + setValue(event.target.value)} + /> + setValue(event.target.value)} + /> + setValue(event.target.value)} + /> +
+ ); + }, +}; diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx new file mode 100644 index 0000000..e8185ee --- /dev/null +++ b/src/components/DatePicker.tsx @@ -0,0 +1,95 @@ +import type { ChangeEventHandler, FocusEventHandler, ReactNode, Ref } from 'react'; +import type { ComponentSize } from './types'; + +type DatePickerKind = 'date' | 'datetime-local' | 'time'; +type Layout = 'stacked' | 'inline'; + +export type DatePickerProps = { + label?: string; + placeholder?: string; + type: DatePickerKind; + size?: ComponentSize; + layout?: Layout; + value: string; + name?: string; + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + inputRef?: Ref; + disabled?: boolean; + required?: boolean; + error?: string; + rightIcon?: ReactNode; + className?: string; + inputClassName?: string; +}; + +export function DatePicker({ + label, + placeholder = '', + type, + size = 'md', + layout = 'stacked', + value, + name, + onChange, + onBlur, + inputRef, + disabled = false, + required = false, + error, + rightIcon, + className = '', + inputClassName = '', +}: Readonly) { + const containerSizeClass = { + sm: 'max-w-xs', + md: 'max-w-sm', + lg: 'max-w-md', + full: 'max-w-none', + }[size]; + + const inputSizeClass = { + sm: 'h-8 !text-xs', + md: 'h-10 text-sm', + lg: 'h-12 text-sm', + full: 'h-10 text-sm', + }[size]; + + const wrapperClass = + layout === 'inline' ? 'inline-flex w-auto items-center gap-2' : 'block w-full gap-1'; + const labelClass = layout === 'inline' ? 'text-xs ui-body-secondary' : ''; + const hasTrailingIcon = Boolean(rightIcon); + const inputWrapperClass = layout === 'inline' ? 'relative' : 'relative mt-1'; + + return ( + + ); +} diff --git a/src/index.ts b/src/index.ts index adec647..e4fbd03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { Button } from './components/Button'; export { Chip } from './components/Chip'; +export { DatePicker } from './components/DatePicker'; export { Dropdown } from './components/Dropdown'; export { Form } from './components/Form'; export { InputField } from './components/InputField'; @@ -8,5 +9,6 @@ export { SidebarNavItem } from './components/SidebarNavItem'; export { Table } from './components/Table'; export type { TableHeader } from './components/Table'; +export type { DatePickerProps } from './components/DatePicker'; export type { ComponentSize } from './components/types'; export type { SortDirection, SortState } from './types/sort';