crisli-picker
Version:
A modern, customizable date and time picker library for React applications with configurable time intervals and booking system support
586 lines (471 loc) ⢠19.2 kB
Markdown
# Crisli Picker
A modern, customizable date and time picker library for React applications with configurable time intervals, inspired by mobile date/time pickers.
Originally created for [Crisli App](https://crisli.app) and now available as an open-source library for the React community.
## š” [Live Demo](https://rupok.github.io/crisli-picker/) | [NPM Package](https://www.npmjs.com/package/crisli-picker) | [GitHub](https://github.com/rupok/crisli-picker)
## Features
### **š” Core Components**
- **Smooth wheel-based pickers** with mobile-like scrolling and momentum
- **Calendar date selection** with intuitive interface
- **Time selection** with hour and minute wheels (**12-hour with AM/PM** or **24-hour formats**)
- **ā° Time step intervals** - Configure 5, 15, 30-minute intervals for appointment booking
- **Generic wheel picker** for custom option lists
- **Horizontal calendar layout** for better space utilization
### **šØ Design & Theming**
- **Light and dark theme support** with customizable colors
- **Responsive design** optimized for mobile, tablet, and desktop
- **Touch and mouse support** with gesture recognition
- **Smooth animations** and transitions
- **Customizable styling** for fonts, colors, and dimensions
### **ā” Developer Experience**
- **TypeScript support** with comprehensive type definitions
- **Works with both JSX and TSX** projects seamlessly
- **Comprehensive test suite** with Jest + React Testing Library
- **Interactive demo application** with live examples
- **Complete documentation** and API reference
- **Modern build system** with Rollup (CommonJS + ESM)
### **š± Accessibility & UX**
- **Mobile-first design** with touch optimization
- **Keyboard navigation** support
- **Screen reader friendly** with proper ARIA labels
- **Momentum scrolling** like native mobile pickers
- **Edge case handling** (null values, date boundaries)
- **š« Disable past dates/times** - perfect for booking and scheduling systems
## Installation
```bash
npm install crisli-picker
# or
yarn add crisli-picker
```
## TypeScript Support
This package includes comprehensive TypeScript definitions. Both JSX and TSX projects are fully supported:
### JSX Projects
```jsx
// Works perfectly - no changes needed
import { DateTimePicker } from 'crisli-picker';
function MyComponent() {
const [date, setDate] = useState(new Date());
return <DateTimePicker value={date} onChange={setDate} />;
}
```
### TSX Projects
```tsx
// Gets full TypeScript support automatically
import { DateTimePicker } from 'crisli-picker';
const MyComponent: React.FC = () => {
const [date, setDate] = useState<Date>(new Date());
return (
<DateTimePicker
value={date} // ā
Type checked
onChange={setDate} // ā
Type checked
theme="light" // ā
Autocomplete: "light" | "dark"
showTime={true} // ā
IntelliSense shows all props
/>
);
};
```
## Usage
### DateTimePicker
```jsx
import { DateTimePicker } from 'crisli-picker';
function MyComponent() {
const [date, setDate] = useState(new Date());
return (
<DateTimePicker
value={date}
onChange={setDate}
showTime={true}
theme="light"
/>
);
}
```
### TimePicker
```jsx
import { TimePicker } from 'crisli-picker';
function MyComponent() {
const [time, setTime] = useState(new Date());
return (
<TimePicker
value={time}
onChange={setTime}
use24Hours={true} // or false for 12-hour format with AM/PM
theme="light"
/>
);
}
```
### ā° Time Step Intervals (NEW!)
Configure time intervals for more efficient time selection - perfect for appointment booking and scheduling systems:
```jsx
import { TimePicker, DateTimePicker, CalendarTimePicker } from 'crisli-picker';
// 15-minute intervals: 00:00, 00:15, 00:30, 00:45, etc.
<TimePicker
value={time}
onChange={setTime}
minuteStep={15} // ā° 15-minute intervals
use24Hours={true}
/>
// 30-minute intervals with 12-hour format
<TimePicker
value={time}
onChange={setTime}
minuteStep={30} // ā° 30-minute intervals
use24Hours={false}
/>
// Custom intervals: 5-minute steps with 2-hour steps
<TimePicker
value={time}
onChange={setTime}
minuteStep={5} // ā° 5-minute intervals
hourStep={2} // ā° 2-hour intervals (00:xx, 02:xx, 04:xx, etc.)
/>
// Works with all time picker components
<DateTimePicker
value={dateTime}
onChange={setDateTime}
minuteStep={15} // ā° Perfect for appointment booking
disablePast={true}
/>
<CalendarTimePicker
value={dateTime}
onChange={setDateTime}
minuteStep={30} // ā° 30-minute scheduling slots
hourStep={1} // ā° Every hour (default)
/>
```
**Perfect for:**
- š
**Appointment Booking** (15 or 30-minute slots)
- š„ **Medical Scheduling** (15-minute intervals)
- š¬ **Event Planning** (30-minute or 1-hour slots)
- š **Service Booking** (Custom intervals)
- š **Meeting Scheduling** (15 or 30-minute meetings)
**Features:**
- ā
**Smart Value Snapping** - Automatically snaps to nearest valid interval
- ā
**Independent Control** - Set different steps for minutes and hours
- ā
**Backward Compatible** - Default behavior unchanged (step = 1)
- ā
**Performance Optimized** - Only generates valid time options
- ā
**Works with All Components** - TimePicker, DateTimePicker, CalendarTimePicker, HorizontalCalendarTimePicker
### 12-Hour Format Support
All time-related components support both 24-hour and 12-hour formats:
```jsx
// 24-hour format (default)
<DateTimePicker use24Hours={true} />
<TimePicker use24Hours={true} />
<CalendarTimePicker use24Hours={true} />
<HorizontalCalendarTimePicker use24Hour={true} />
// 12-hour format with AM/PM wheel
<DateTimePicker use24Hours={false} />
<TimePicker use24Hours={false} />
<CalendarTimePicker use24Hours={false} />
<HorizontalCalendarTimePicker use24Hour={false} />
```
### CalendarTimePicker
```jsx
import { CalendarTimePicker } from 'crisli-picker';
function MyComponent() {
const [dateTime, setDateTime] = useState(new Date());
return (
<CalendarTimePicker
value={dateTime}
onChange={setDateTime}
theme="light" // or "dark"
showTime={true} // can be set to false to hide time selection
/>
);
}
```
### HorizontalCalendarTimePicker
```jsx
import { HorizontalCalendarTimePicker } from 'crisli-picker';
function MyComponent() {
const [dateTime, setDateTime] = useState(new Date());
return (
<HorizontalCalendarTimePicker
value={dateTime}
onChange={setDateTime}
theme="light" // or "dark"
showTime={true} // can be set to false to hide time selection
use24Hour={true} // set to false for 12-hour format with AM/PM
/>
);
}
```
### š« Disable Past Dates/Times
Perfect for booking systems, appointment scheduling, and any scenario where past dates/times should not be selectable:
```jsx
// Disable past dates and times - only future selections allowed
<DateTimePicker
value={dateTime}
onChange={setDateTime}
disablePast={true} // š« Prevents past date/time selection
use24Hours={false} // Works with both 12-hour and 24-hour formats
/>
// Time-only picker with past time restriction (for today)
<TimePicker
value={time}
onChange={setTime}
disablePast={true} // š« Only future times selectable
/>
// Calendar with future-only dates
<CalendarTimePicker
value={dateTime}
onChange={setDateTime}
disablePast={true} // š« Past dates are grayed out and unselectable
showTime={true}
/>
// Horizontal layout for booking interfaces
<HorizontalCalendarTimePicker
value={dateTime}
onChange={setDateTime}
disablePast={true} // š« Perfect for appointment booking
use24Hour={true}
/>
```
**Features:**
- ā
**Past dates** are visually disabled (grayed out with strikethrough)
- ā
**Past times** are disabled when today is selected
- ā
**Click/touch prevention** on disabled items
- ā
**Automatic scrolling** skips disabled items
- ā
**Visual feedback** with reduced opacity and strikethrough text
- ā
**Works with all components** (DateTimePicker, TimePicker, CalendarTimePicker, HorizontalCalendarTimePicker)
### WheelPicker
```jsx
import { WheelPicker } from 'crisli-picker';
function MyComponent() {
const [selectedOption, setSelectedOption] = useState('option1');
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
];
return (
<WheelPicker
items={options}
value={selectedOption}
onChange={setSelectedOption}
label="Select an option"
theme="light"
/>
);
}
```
## API Reference
### DateTimePicker
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Date` | `new Date()` | The selected date and time |
| `onChange` | `function` | required | Callback when date changes |
| `showTime` | `boolean` | `true` | Whether to show time picker |
| `use24Hours` | `boolean` | `true` | Whether to use 24-hour format |
| `minuteStep` | `number` | `1` | ā° **NEW!** Step interval for minutes (e.g., 15 for 15-minute intervals) |
| `hourStep` | `number` | `1` | ā° **NEW!** Step interval for hours (e.g., 2 for 2-hour intervals) |
| `disablePast` | `boolean` | `false` | Whether to disable past dates/times |
| `wheelProps` | `object` | `{}` | Props to pass to all wheels |
| `theme` | `string` | `'light'` | Theme for the picker ('light' or 'dark') |
### TimePicker
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Date` | `new Date()` | The selected time |
| `onChange` | `function` | required | Callback when time changes |
| `use24Hours` | `boolean` | `true` | Whether to use 24-hour format |
| `minuteStep` | `number` | `1` | ā° **NEW!** Step interval for minutes (e.g., 15 for 15-minute intervals) |
| `hourStep` | `number` | `1` | ā° **NEW!** Step interval for hours (e.g., 2 for 2-hour intervals) |
| `disablePast` | `boolean` | `false` | Whether to disable past times (for today) |
| `wheelProps` | `object` | `{}` | Props to pass to all wheels |
| `theme` | `string` | `'light'` | Theme for the picker ('light' or 'dark') |
### CalendarTimePicker
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Date` | `new Date()` | The selected date and time |
| `onChange` | `function` | required | Callback when date/time changes |
| `showTime` | `boolean` | `true` | Whether to show time picker |
| `use24Hours` | `boolean` | `true` | Whether to use 24-hour format |
| `minuteStep` | `number` | `1` | ā° **NEW!** Step interval for minutes (e.g., 15 for 15-minute intervals) |
| `hourStep` | `number` | `1` | ā° **NEW!** Step interval for hours (e.g., 2 for 2-hour intervals) |
| `disablePast` | `boolean` | `false` | Whether to disable past dates/times |
| `wheelProps` | `object` | `{}` | Props to pass to all wheels |
| `theme` | `string` | `'light'` | Theme for the picker ('light' or 'dark') |
### HorizontalCalendarTimePicker
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Date` | `new Date()` | The selected date and time |
| `onChange` | `function` | required | Callback when date/time changes |
| `showTime` | `boolean` | `true` | Whether to show time picker |
| `use24Hour` | `boolean` | `true` | Whether to use 24-hour format |
| `minuteStep` | `number` | `1` | ā° **NEW!** Step interval for minutes (e.g., 15 for 15-minute intervals) |
| `hourStep` | `number` | `1` | ā° **NEW!** Step interval for hours (e.g., 2 for 2-hour intervals) |
| `disablePast` | `boolean` | `false` | Whether to disable past dates/times |
| `wheelProps` | `object` | `{}` | Props to pass to all wheels |
| `theme` | `string` | `'light'` | Theme for the picker ('light' or 'dark') |
| `className` | `string` | `''` | Additional CSS class for the component |
| `style` | `object` | `{}` | Additional inline styles for the component |
| `timeFormat` | `string` | `'HH:mm'` | Format for displaying time |
### WheelPicker
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `array` | `[]` | Array of items to display in the picker |
| `value` | `any` | required | Currently selected value |
| `onChange` | `function` | required | Callback when value changes |
| `label` | `string` | `undefined` | Label for the picker |
| `wheelProps` | `object` | `{}` | Props to pass to the wheel |
| `theme` | `string` | `'light'` | Theme for the picker ('light' or 'dark') |
### Wheel
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `array` | required | Array of items to display in the wheel. Each item can have a `disabled` property |
| `value` | `any` | required | Currently selected value |
| `onChange` | `function` | required | Callback when value changes |
| `itemHeight` | `number` | `40` | Height of each item in pixels |
| `fontSize` | `string` | `'16px'` | Font size for the items |
| `fontWeight` | `string` | `'400'` | Font weight for the items |
| `textColor` | `string` | `'#666'` | Text color for the items |
| `selectedTextColor` | `string` | `'#000'` | Text color for the selected item |
| `highlightColor` | `string` | `'rgba(0, 0, 0, 0.05)'` | Background color for the selected item highlight |
| `highlightBorderColor` | `string` | `'rgba(0, 0, 0, 0.1)'` | Border color for the selected item highlight |
**Item Object Structure:**
```jsx
{
value: any, // The value of the item
label: string, // Display text for the item
disabled?: boolean // Optional: whether the item is disabled
}
```
## š Live Demo
Experience Crisli Picker with our comprehensive demo applications:
### **šØ Interactive Demo** (Recommended)
**URL**: http://localhost:3001 (when running locally) | [Live Demo](https://rupok.github.io/crisli-picker/)
A modern, full-featured demo built with Vite + React featuring:
- **š Home Page**: Hero section, feature overview, and quick component demos
- **š§© Components Page**: Detailed showcase of all 5 components with live code examples
- **š Examples Page**: Real-world use cases including appointment booking form
- **š Documentation Page**: Complete API reference and TypeScript support guide
- **š Theme Switching**: Toggle between light and dark themes
- **š± Responsive Design**: Optimized for desktop, tablet, and mobile
- **ā” Hot Reload**: Instant updates during development
### **š Simple Demo**
**URL**: http://localhost:3000 (when running locally)
A lightweight HTML demo for quick testing and basic component exploration.
## Development
### Running the Demo Locally
```bash
# Install dependencies
npm install
# Install demo dependencies
cd demo && npm install && cd ..
# Start development server (builds library + runs demo)
npm run dev
```
**Available URLs:**
- **Vite Demo**: http://localhost:3001 (full-featured)
- **Simple Demo**: http://localhost:3000 (basic HTML)
### Demo-Only Development
```bash
# Run only the Vite demo (without library rebuild)
npm run demo:dev
# Build demo for production
npm run demo:build
# Preview production demo build
npm run demo:preview
```
### Building the Library
```bash
# Build the library
npm run build
# Build with watch mode
npm run build:watch
```
### Testing
Comprehensive test suite using Jest + React Testing Library:
```bash
# Run tests
npm test
# Run tests in watch mode
npm test:watch
# Run tests with coverage
npm test:coverage
```
**Test Coverage:**
- ā
**Component rendering** and prop handling
- ā
**User interactions** (clicks, scrolls, touch events)
- ā
**Theme switching** and styling
- ā
**Edge cases** (null values, empty arrays)
- ā
**Accessibility** and keyboard navigation
**Test Files:**
- `tests/components/Wheel.test.jsx` - Core wheel component tests
- `tests/components/DateTimePicker.test.jsx` - Date/time picker tests
- `tests/components/WheelPicker.test.jsx` - Generic wheel picker tests
- `tests/setup.js` - Test environment configuration
### Type Checking
```bash
# Check TypeScript definitions
npm run type-check
```
### Linting
```bash
# Lint code
npm run lint
# Fix linting issues
npm run lint:fix
```
## š Project Structure
```
crisli-picker/
āāā src/ # Library source code
ā āāā components/ # React components
ā ā āāā Wheel.jsx # Core wheel component
ā ā āāā DateTimePicker.jsx # Date & time picker
ā ā āāā TimePicker.jsx # Time-only picker
ā ā āāā WheelPicker.jsx # Generic wheel picker
ā ā āāā CalendarTimePicker.jsx
ā ā āāā HorizontalCalendarTimePicker.jsx
ā āāā types/ # TypeScript definitions
ā ā āāā index.d.ts # Main type definitions
ā ā āāā Wheel.d.ts # Wheel component types
ā ā āāā tsconfig.json # TypeScript config
ā āāā index.js # Main entry point
āāā demo/ # Vite demo application
ā āāā src/
ā ā āāā components/ # Demo-specific components
ā ā āāā pages/ # Demo pages (Home, Components, etc.)
ā ā āāā App.jsx # Main demo app
ā ā āāā main.jsx # Demo entry point
ā āāā package.json # Demo dependencies
ā āāā vite.config.js # Vite configuration
āāā tests/ # Test suite
ā āāā components/ # Component tests
ā āāā setup.js # Test configuration
āāā dist/ # Built library files
ā āāā index.js # CommonJS build
ā āāā index.esm.js # ES Module build
ā āāā *.map # Source maps
āāā demo-full.html # Simple HTML demo
āāā demo-server.js # Simple demo server
āāā package.json # Main package configuration
āāā rollup.config.js # Build configuration
āāā README.md # This file
```
## š Development Workflow
### **For Library Development:**
1. Make changes to components in `src/components/`
2. Add/update TypeScript definitions in `src/types/`
3. Write tests in `tests/components/`
4. Run `npm run dev` to test in demo
5. Run `npm test` to verify tests pass
6. Run `npm run build` to build library
### **For Demo Development:**
1. Make changes to demo in `demo/src/`
2. Run `npm run demo:dev` for demo-only development
3. Test responsiveness and theme switching
4. Build with `npm run demo:build`
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Add tests for your changes
5. Ensure tests pass (`npm test`)
6. Ensure type checking passes (`npm run type-check`)
7. Commit your changes (`git commit -m 'Add amazing feature'`)
8. Push to the branch (`git push origin feature/amazing-feature`)
9. Open a Pull Request
## License
MIT