|
| 1 | +# React Native Date Time Spinner - Copilot Instructions |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +A lightweight, pure JavaScript React Native library providing spinner-style date and time pickers. No native linking required. Built as a monorepo with the library in `src/` and Expo example app in `examples/example-expo/`. |
| 6 | + |
| 7 | +## React Native Performance |
| 8 | + |
| 9 | +When reviewing or writing React Native code, apply the optimization guidelines from: |
| 10 | + |
| 11 | +- ~/.github/agent-skills/skills/react-native-best-practices/SKILL.md (main reference) |
| 12 | +- ~/.github/agent-skills/skills/react-native-best-practices/references/ (detailed skills) |
| 13 | + |
| 14 | +Key patterns: |
| 15 | + |
| 16 | +- Use FlashList over FlatList for large lists |
| 17 | +- Avoid barrel exports |
| 18 | +- Profile before optimizing |
| 19 | + |
| 20 | +## Architecture |
| 21 | + |
| 22 | +### Component Hierarchy |
| 23 | + |
| 24 | +- **`DateTimeSpinner`** (entry point): Orchestrates date/datetime picking with configurable columns |
| 25 | + |
| 26 | + - Wraps multiple **`DurationScroll`** instances (one per column: day, month, year, hour, minute) |
| 27 | + - Manages state synchronization between columns (e.g., adjusting days when month changes) |
| 28 | + - Handles min/max date constraints and value clamping |
| 29 | + |
| 30 | +- **`DurationScroll`**: Low-level FlatList-based scrollable picker column |
| 31 | + - Implements infinite scrolling via `repeatNumbersNTimes` prop (repeats item array N times) |
| 32 | + - Calculates scroll offsets and snaps to items based on `interval` and `padWithNItems` |
| 33 | + - Location: `src/components/DurationScroll/` |
| 34 | + |
| 35 | +### Key Architectural Patterns |
| 36 | + |
| 37 | +1. **Forwarded refs**: Both components expose imperative handles (`setValue`, `reset`, `latestValue`) via `forwardRef`/`useImperativeHandle` |
| 38 | +2. **Utility functions are pure**: All `src/utils/` functions are stateless helpers (e.g., `generateNumbers.ts`, `getInitialScrollIndex.ts`) |
| 39 | +3. **Styles are generated functions**: `generateStyles()` in `styles.ts` merges custom styles with defaults at render time |
| 40 | +4. **Optional gradient overlays**: Components accept `LinearGradient` and `MaskedView` props (not bundled) for fade effects |
| 41 | + |
| 42 | +## Development Workflow |
| 43 | + |
| 44 | +### Setup & Running |
| 45 | + |
| 46 | +```bash |
| 47 | +yarn setup # Install all dependencies (root + workspaces) |
| 48 | +yarn start # Start Expo dev server (example-expo) |
| 49 | +yarn android/ios # Run on device (from example-expo workspace) |
| 50 | +yarn build # Build library with react-native-builder-bob |
| 51 | +``` |
| 52 | + |
| 53 | +### Testing |
| 54 | + |
| 55 | +```bash |
| 56 | +yarn test # Run Jest tests (silent mode, force exit) |
| 57 | +yarn ts # TypeScript type checking (no emit) |
| 58 | +yarn lint:fix # Auto-fix ESLint issues |
| 59 | +``` |
| 60 | + |
| 61 | +- Tests live in `src/tests/` with mocks in `src/tests/__mocks__/` |
| 62 | +- Test file naming: matches source file name (e.g., `DurationScroll.test.tsx`) |
| 63 | +- Use `@testing-library/react-native` for component tests |
| 64 | + |
| 65 | +### Build System |
| 66 | + |
| 67 | +- **react-native-builder-bob** generates 3 outputs: `commonjs`, `module`, `typescript` (see `package.json` config) |
| 68 | +- Entry points: `main`, `module`, `types` fields point to `dist/` (generated on `yarn prepare`) |
| 69 | +- Source lives in `src/`, excluded: `src/tests`, `examples/` |
| 70 | + |
| 71 | +## Code Conventions |
| 72 | + |
| 73 | +### Component Props Pattern |
| 74 | + |
| 75 | +```tsx |
| 76 | +// Always destructure props with defaults at top of component |
| 77 | +const { |
| 78 | + allowFontScaling = false, |
| 79 | + padWithNItems = 2, |
| 80 | + onDateChange, |
| 81 | + // ... rest |
| 82 | +} = props; |
| 83 | +``` |
| 84 | + |
| 85 | +### TypeScript Patterns |
| 86 | + |
| 87 | +- Export types from component index files: `export { ComponentProps, ComponentRef } from "./Component"` |
| 88 | +- Ref types: `ComponentRef` interface with imperative methods |
| 89 | +- Props types: `ComponentProps` interface |
| 90 | +- Value types: `ComponentValue` type (e.g., `DateTimeSpinnerValue` with `date`, `day`, `month`, etc.) |
| 91 | + |
| 92 | +### Styling |
| 93 | + |
| 94 | +- Use `CustomDateTimeSpinnerStyles` type for style props (subset of React Native styles) |
| 95 | +- Theme support via `theme?: "light" | "dark"` style prop |
| 96 | +- Always merge custom styles with defaults: `...customStyles?.pickerItem` |
| 97 | + |
| 98 | +### Date Handling |
| 99 | + |
| 100 | +- Internal state uses separate `day`, `month`, `year`, `hour`, `minute` numbers |
| 101 | +- Constraining logic in `getSafeInitialDateValue.ts` handles invalid dates (e.g., Feb 31 → Feb 28) |
| 102 | +- Min/max dates are swapped if provided in wrong order (see `DateTimeSpinner.tsx` line ~95) |
| 103 | +- Normalize dates to midnight when comparing date-only values |
| 104 | + |
| 105 | +### Accessibility |
| 106 | + |
| 107 | +- All picker columns have `accessibilityRole="adjustable"` for VoiceOver/TalkBack support |
| 108 | +- Each column gets descriptive labels: "Day", "Month", "Year", "Hour", "Minute", "Date" |
| 109 | +- Users can override labels via `accessibilityLabels` prop for internationalization |
| 110 | +- Container uses auto-generated label: "Date picker" or "Date and time picker" |
| 111 | +- Time separator marked as `accessible={false}` to avoid screen reader confusion |
| 112 | + |
| 113 | +## File Organization |
| 114 | + |
| 115 | +``` |
| 116 | +src/ |
| 117 | +├── index.ts # Public API exports |
| 118 | +├── components/ |
| 119 | +│ ├── DateTimeSpinner/ |
| 120 | +│ │ ├── DateTimeSpinner.tsx # Main component (~1200 lines) |
| 121 | +│ │ ├── types.ts # Props, Ref, Value types |
| 122 | +│ │ ├── styles.ts # generateStyles() function |
| 123 | +│ │ └── index.ts # Component exports |
| 124 | +│ └── DurationScroll/ # Reusable scroll column |
| 125 | +└── utils/ # Pure functions only |
| 126 | +``` |
| 127 | + |
| 128 | +## Common Tasks |
| 129 | + |
| 130 | +### Adding a new prop to DateTimeSpinner |
| 131 | + |
| 132 | +1. Add to `DateTimeSpinnerProps` in `types.ts` |
| 133 | +2. Destructure with default in component |
| 134 | +3. Pass through to `DurationScroll` if needed |
| 135 | +4. Update README.md Props table |
| 136 | +5. Add example to `examples/example-expo/App.tsx` |
| 137 | + |
| 138 | +### Modifying date constraint logic |
| 139 | + |
| 140 | +- Edit `src/utils/getSafeInitialDateValue.ts` (handles clamping/validation) |
| 141 | +- Update tests in `src/tests/getSafeInitialDateValue.test.ts` |
| 142 | +- Date parts are clamped independently then validated together |
| 143 | + |
| 144 | +### Debugging scroll behavior |
| 145 | + |
| 146 | +- Check `getInitialScrollIndex.ts` for initial positioning math |
| 147 | +- Check `getDurationAndIndexFromScrollOffset.ts` for scroll-to-value conversion |
| 148 | +- Verify `repeatNumbersNTimes` calculation in `DurationScroll.tsx` line ~75 (dynamic repeat logic) |
| 149 | + |
| 150 | +## Dependencies |
| 151 | + |
| 152 | +- **Peer deps**: React >=18.2, React Native >=0.72 |
| 153 | +- **Optional**: `expo-linear-gradient` or `react-native-linear-gradient` for gradients |
| 154 | +- **Dev**: Jest, Testing Library, react-native-builder-bob |
| 155 | +- **Example app**: Uses `date-fns` for formatting, Expo 54+ |
| 156 | + |
| 157 | +## Key Gotchas |
| 158 | + |
| 159 | +- Infinite scroll requires `repeatNumbersNTimes >= 2` (enforced in `DurationScroll.tsx`) |
| 160 | +- Single-item pickers force `repeatNumbersNTimes = 1` to avoid empty lists |
| 161 | +- `formatDateToParts` overrides individual `padDayWithZero`/`padMonthWithZero` props |
| 162 | +- `dateTimeOrder` only applies in `mode="datetime"` (ignored in `mode="date"`) |
0 commit comments