cleanhaus-calendar 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,8 @@
1
1
  # cleanhaus-calendar
2
2
 
3
- A production-ready, cross-platform calendar component for React Native and Next.js. Features Month, Week, and Day views with horizontal time positioning and multi-day event spanning.
3
+ Cross-platform calendar component for React Native and Next.js with Month, Week, and Day views. Features horizontal time positioning, multi-day event spanning, and type-based event rendering.
4
4
 
5
- ## 🚀 Platform Support
6
-
7
- - ✅ **React Native** (iOS/Android)
8
- - ✅ **Next.js** (Web with react-native-web)
9
-
10
- ## 📦 Installation
5
+ ## Installation
11
6
 
12
7
  ```bash
13
8
  npm install cleanhaus-calendar
@@ -19,23 +14,24 @@ npm install cleanhaus-calendar
19
14
  npm install react react-native react-native-web react-native-reanimated dayjs calendarize
20
15
  ```
21
16
 
22
- **Requirements:**
23
- - `react`: >=18.0.0 (supports React 18 & 19)
24
- - `react-native`: >=0.70.0
25
- - `react-native-web`: >=0.19.0
26
- - `react-native-reanimated`: >=3.0.0
27
- - `dayjs`: ^1.11.0
28
- - `calendarize`: ^1.1.0
29
- - `node`: >=18.0.0
17
+ **Compatibility:**
18
+ - React >=18.0.0
19
+ - React Native >=0.70.0
20
+ - react-native-web >=0.19.0 (optional, for web)
21
+ - react-native-reanimated >=3.0.0
22
+ - dayjs ^1.11.0
23
+ - calendarize ^1.1.0
24
+ - Node >=18.0.0
30
25
 
31
- ## ⚙️ Setup
26
+ ## Quick Start
32
27
 
33
- ### React Native (Expo/RN)
28
+ ### React Native
34
29
 
35
- No additional configuration needed! Just install and use:
30
+ No additional configuration needed. Install and use:
36
31
 
37
32
  ```tsx
38
33
  import { Calendar, CalendarEvent } from "cleanhaus-calendar";
34
+ import { useState } from "react";
39
35
 
40
36
  const events: CalendarEvent[] = [
41
37
  {
@@ -48,24 +44,29 @@ const events: CalendarEvent[] = [
48
44
  },
49
45
  ];
50
46
 
51
- <Calendar
52
- events={events}
53
- view="month"
54
- date={new Date()}
55
- onDateChange={setDate}
56
- onEventPress={(event) => console.log(event)}
57
- />
47
+ function App() {
48
+ const [date, setDate] = useState(new Date());
49
+
50
+ return (
51
+ <Calendar
52
+ events={events}
53
+ view="month"
54
+ date={date}
55
+ onDateChange={setDate}
56
+ onEventPress={(event) => {
57
+ // Handle event press
58
+ }}
59
+ />
60
+ );
61
+ }
58
62
  ```
59
63
 
60
- ### Next.js Setup
64
+ ### Next.js
61
65
 
62
- **1. Install dependencies:**
63
- ```bash
64
- npm install cleanhaus-calendar react react-native react-native-web react-native-reanimated dayjs calendarize
65
- ```
66
+ **1. Configure Next.js:**
66
67
 
67
- **2. Update `next.config.ts`:**
68
68
  ```typescript
69
+ // next.config.ts
69
70
  import type { NextConfig } from "next";
70
71
 
71
72
  const withCalendar = require("cleanhaus-calendar/next-plugin");
@@ -77,7 +78,10 @@ const nextConfig: NextConfig = {
77
78
  export default withCalendar(nextConfig);
78
79
  ```
79
80
 
80
- **3. Update `package.json` dev script (Next.js 16+):**
81
+ **2. Development (Next.js 16+):**
82
+
83
+ Next.js 16+ uses Turbopack by default for development, but this package requires webpack configuration. Use the `--webpack` flag during development:
84
+
81
85
  ```json
82
86
  {
83
87
  "scripts": {
@@ -86,105 +90,69 @@ export default withCalendar(nextConfig);
86
90
  }
87
91
  ```
88
92
 
89
- **4. Use the component:**
90
- ```tsx
91
- "use client"; // Required for App Router
93
+ **Note:** Production builds automatically use webpack, so no additional configuration is needed for deployment.
92
94
 
93
- import { Calendar, CalendarEvent } from "cleanhaus-calendar";
94
- import { useState } from "react";
95
-
96
- export default function MyPage() {
97
- const [view, setView] = useState<ViewMode>("month");
98
- const [date, setDate] = useState(new Date());
99
-
100
- return (
101
- <Calendar
102
- events={events}
103
- view={view}
104
- date={date}
105
- onDateChange={setDate}
106
- onEventPress={(event) => console.log(event)}
107
- onViewChange={setView}
108
- />
109
- );
110
- }
111
- ```
112
-
113
- **Note:** Next.js 16+ uses Turbopack by default. The plugin requires webpack, so use `--webpack` flag.
114
-
115
- ## 📖 Usage
116
-
117
- ### Basic Example
95
+ **3. Use the component:**
118
96
 
119
97
  ```tsx
98
+ "use client";
99
+
120
100
  import { Calendar, CalendarEvent, ViewMode } from "cleanhaus-calendar";
121
101
  import { useState } from "react";
122
102
 
123
- const events: CalendarEvent[] = [
124
- {
125
- id: "1",
126
- eventId: "property-1",
127
- title: "Booking",
128
- start: new Date(2025, 0, 15, 10, 0),
129
- end: new Date(2025, 0, 20, 14, 0),
130
- meta: { type: "property" },
131
- },
132
- ];
133
-
134
- function MyCalendar() {
103
+ export default function CalendarPage() {
135
104
  const [view, setView] = useState<ViewMode>("month");
136
105
  const [date, setDate] = useState(new Date());
137
106
 
107
+ const events: CalendarEvent[] = [
108
+ {
109
+ id: "1",
110
+ eventId: "property-1",
111
+ title: "Booking",
112
+ start: new Date(2025, 0, 15, 10, 0),
113
+ end: new Date(2025, 0, 20, 14, 0),
114
+ meta: { type: "property" },
115
+ },
116
+ ];
117
+
138
118
  return (
139
119
  <Calendar
140
120
  events={events}
141
121
  view={view}
142
122
  date={date}
143
123
  onDateChange={setDate}
144
- onEventPress={(event) => console.log(event)}
145
124
  onViewChange={setView}
125
+ onEventPress={(event) => {
126
+ // Handle event press
127
+ }}
146
128
  />
147
129
  );
148
130
  }
149
131
  ```
150
132
 
151
- ### Custom Cleaning Icon
152
-
153
- The package includes a default sparkle icon (✨) for cleaning events. Override it:
154
-
155
- ```tsx
156
- import sparksIcon from "./assets/sparks.png";
157
-
158
- <Calendar
159
- events={events}
160
- cleaningIcon={sparksIcon} // Optional: custom icon
161
- // ... other props
162
- />
163
- ```
164
-
165
- ## 🔄 Data Format
133
+ ## Data Format
166
134
 
167
135
  Events must follow this structure:
168
136
 
169
137
  ```typescript
170
138
  interface CalendarEvent {
171
- id: string; // Required: Unique identifier
172
- eventId: string; // Required: Group identifier (e.g., "property-1")
173
- title: string; // Required: Event title
174
- start: Date; // Required: Must be Date object (not string!)
175
- end: Date; // Required: Must be Date object (not string!)
139
+ id: string; // Unique identifier
140
+ eventId: string; // Group identifier (e.g., "property-1")
141
+ title: string; // Event title
142
+ start: Date; // Start date/time (must be Date object)
143
+ end: Date; // End date/time (must be Date object)
176
144
  meta?: {
177
145
  type?: "property" | "cleaning" | "service" | "otherService" | "unassigned";
178
- jobTypeId?: number; // For cleaning: 1 = cleaning, 2-4 = service types
179
146
  [key: string]: any;
180
147
  };
181
148
  }
182
149
  ```
183
150
 
184
- ### Transform API Data
151
+ **Important:** Always convert date strings to `Date` objects. The component does not accept string dates.
152
+
153
+ ### Transforming API Data
185
154
 
186
155
  ```typescript
187
- // Transform API response to CalendarEvent format
188
156
  function transformApiEvents(apiData: ApiEvent[]): CalendarEvent[] {
189
157
  return apiData.map((item) => ({
190
158
  id: item.id.toString(),
@@ -199,77 +167,75 @@ function transformApiEvents(apiData: ApiEvent[]): CalendarEvent[] {
199
167
  }
200
168
  ```
201
169
 
202
- **Important:** Always convert date strings to `Date` objects. The component does not accept string dates.
170
+ ## API Reference
171
+
172
+ ### Calendar Props
203
173
 
204
- ## 🎯 Key Props
174
+ | Prop | Type | Required | Default | Description |
175
+ |------|------|----------|---------|-------------|
176
+ | `events` | `CalendarEvent[]` | Yes | - | Array of events to display |
177
+ | `date` | `Date` | Yes | - | Current date/month |
178
+ | `onDateChange` | `(date: Date) => void` | Yes | - | Called when date changes |
179
+ | `onEventPress` | `(event: CalendarEvent) => void` | Yes | - | Called when event is pressed |
180
+ | `view` | `"month" \| "week" \| "day"` | No | `"month"` | View mode |
181
+ | `onViewChange` | `(view: ViewMode) => void` | No | - | Called when view mode changes |
182
+ | `onDateTimeChange` | `(dateTime: Date) => void` | No | - | Unified handler for date+time changes (navigates to day view) |
183
+ | `isLoading` | `boolean` | No | `false` | Show loading spinner |
184
+ | `theme` | `Partial<CalendarTheme>` | No | - | Custom theme override |
185
+ | `availableProperties` | `Array<{ id: number }>` | No | - | Properties for consistent color assignment |
186
+ | `propertiesToShow` | `Array<{ id: number; name?: string }>` | No | - | Properties to show in DayView lanes |
187
+ | `propertyColors` | `string[]` | No | - | Custom property colors array |
188
+ | `propertyColorsDark` | `string[]` | No | - | Custom dark property colors array |
189
+ | `cleaningIcon` | `any` | No | - | Custom icon for cleaning events |
190
+ | `showFAB` | `boolean` | No | `false` | Show floating action button |
191
+ | `onFABPress` | `() => void` | No | - | FAB press handler |
192
+ | `fabStyle` | `ViewStyle` | No | - | Custom FAB styles |
193
+ | `renderFAB` | `() => React.ReactElement \| null` | No | - | Custom FAB component renderer |
194
+ | `autoScrollToNow` | `boolean` | No | `false` | Auto-scroll to current time in day view |
205
195
 
206
- | Prop | Type | Required | Description |
207
- |------|------|----------|-------------|
208
- | `events` | `CalendarEvent[]` | Yes | Array of events |
209
- | `view` | `"month" \| "week" \| "day"` | No | View mode (default: `"month"`) |
210
- | `date` | `Date` | Yes | Current date/month |
211
- | `onDateChange` | `(date: Date) => void` | Yes | Date navigation handler |
212
- | `onEventPress` | `(event: CalendarEvent) => void` | Yes | Event press handler |
213
- | `onViewChange` | `(view: ViewMode) => void` | No | View mode change handler |
214
- | `cleaningIcon` | `any` | No | Custom icon for cleaning events |
215
- | `theme` | `CalendarTheme` | No | Custom theme |
216
- | `availableProperties` | `Property[]` | No | Properties for color assignment |
196
+ ### Exports
217
197
 
218
- See [full props reference](#props-reference) below.
198
+ - `Calendar` - Main calendar component
199
+ - `CalendarEvent` - Event type
200
+ - `ViewMode` - View mode type (`"day" | "week" | "month"`)
201
+ - `MonthView`, `WeekView`, `DayView` - Individual view components
202
+ - `CalendarFAB` - Floating action button component
203
+ - Utilities: `dateUtils`, `weekDayUtils`, `theme`, `propertyColors`
204
+ - Hooks: `useSwipeGesture`
219
205
 
220
- ## 🎨 Features
206
+ ## Features
221
207
 
222
208
  - **Month View**: Calendar grid with event bars and swipe navigation
223
209
  - **Week View**: 7-day view with time-based positioning
224
210
  - **Day View**: Single-day view with property lanes
225
211
  - **Multi-day Events**: Continuous bars across day cells
226
- - **Type-based Rendering**: Different styles for property, cleaning, service events
227
- - **Built-in Assets**: Default sparkle icon (✨) for cleaning events
228
-
229
- ## 🐛 Troubleshooting
212
+ - **Type-based Rendering**: Different styles for different event types
213
+ - **Theme Customization**: Customizable colors and styles
214
+ - **Cross-platform**: Works on React Native (iOS/Android) and Web (Next.js)
215
+ - **SSR-safe**: Server-side rendering compatible
230
216
 
231
- ### Events not appearing
232
- - ✅ Ensure `start` and `end` are `Date` objects (not strings)
233
- - ✅ Check `eventId` is set correctly
234
- - ✅ Verify `containerHeight` is sufficient (minimum 400px)
217
+ ## Troubleshooting
235
218
 
236
- ### Next.js Issues
219
+ **Events not appearing:**
220
+ - Ensure `start` and `end` are `Date` objects (not strings)
221
+ - Verify `eventId` is set correctly
222
+ - Check that events fall within the visible date range
237
223
 
238
- **Module not found:**
239
- - Restart dev server: `npm run dev -- --webpack`
240
- - Clear cache: `rm -rf .next`
224
+ **Next.js issues:**
225
+ - Use `--webpack` flag for development: `npm run dev -- --webpack`
226
+ - Clear Next.js cache: `rm -rf .next`
227
+ - Ensure `react-native-web` is installed
228
+ - Verify the plugin is correctly applied in `next.config.ts`
241
229
 
242
- **Element type is invalid:**
243
- - Ensure `--webpack` flag is used
244
- - Verify `react-native-web` is installed
245
-
246
- **Turbopack error:**
247
- - ✅ Use `npm run dev -- --webpack`
248
- - ✅ Or add `turbopack: {}` to `next.config.ts`
249
-
250
- ## 📚 Props Reference
251
-
252
- ### Calendar Props
253
-
254
- | Prop | Type | Required | Default | Description |
255
- |------|------|----------|---------|-------------|
256
- | `events` | `CalendarEvent[]` | Yes | - | Array of events |
257
- | `view` | `ViewMode` | No | `"month"` | View mode |
258
- | `date` | `Date` | Yes | - | Current date |
259
- | `onDateChange` | `(date: Date) => void` | Yes | - | Date change handler |
260
- | `onEventPress` | `(event: CalendarEvent) => void` | Yes | - | Event press handler |
261
- | `onViewChange` | `(view: ViewMode) => void` | No | - | View change handler |
262
- | `isLoading` | `boolean` | No | `false` | Show loading spinner |
263
- | `theme` | `CalendarTheme` | No | - | Custom theme |
264
- | `availableProperties` | `Property[]` | No | - | Properties for colors |
265
- | `cleaningIcon` | `any` | No | - | Custom cleaning icon |
266
- | `showFAB` | `boolean` | No | `false` | Show floating action button |
267
- | `autoScrollToNow` | `boolean` | No | `false` | Auto-scroll to current time |
230
+ **Module not found errors:**
231
+ - Restart the development server
232
+ - Clear node_modules and reinstall: `rm -rf node_modules && npm install`
268
233
 
269
- ## 📄 License
234
+ ## License
270
235
 
271
236
  MIT
272
237
 
273
- ---
238
+ ## Links
274
239
 
275
- **Version**: 1.0.0
240
+ - [Repository](https://github.com/cleanhaus/calendar-component)
241
+ - [Issues](https://github.com/cleanhaus/calendar-component/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cleanhaus-calendar",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Cross-platform calendar component for React Native and Web",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -19,7 +19,7 @@
19
19
  "files": [
20
20
  "**/*.ts",
21
21
  "**/*.tsx",
22
- "shared/**/*.png",
22
+ "src/shared/**/*.png",
23
23
  "README.md",
24
24
  "LICENSE",
25
25
  "CHANGELOG.md",
package/src/Calendar.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  // React Native Calendar Component - Uses only RN components
2
- import React, { useState, useCallback } from "react";
2
+ import React, { useState, useCallback, useEffect } from "react";
3
3
  import {
4
4
  StyleSheet,
5
5
  View,
@@ -7,6 +7,7 @@ import {
7
7
  Dimensions,
8
8
  ActivityIndicator,
9
9
  } from "react-native";
10
+ import type { LayoutChangeEvent } from "react-native";
10
11
  import { CalendarProps, CalendarEvent } from "./types";
11
12
  import { MonthView } from "./MonthView";
12
13
  import { DayView } from "./DayView";
@@ -18,6 +19,7 @@ import {
18
19
  DEFAULT_PROPERTY_COLORS,
19
20
  DEFAULT_PROPERTY_COLORS_DARK,
20
21
  } from "./utils/propertyColors";
22
+ import { validateCalendarProps, logValidationErrors } from "./utils/validation";
21
23
 
22
24
  export const CustomCalendar: React.FC<CalendarProps> = ({
23
25
  events = [],
@@ -40,6 +42,20 @@ export const CustomCalendar: React.FC<CalendarProps> = ({
40
42
  autoScrollToNow = false,
41
43
  cleaningIcon,
42
44
  }) => {
45
+ // Validate props in development mode
46
+ useEffect(() => {
47
+ if (process.env.NODE_ENV !== "production") {
48
+ const errors = validateCalendarProps({
49
+ events,
50
+ date,
51
+ onDateChange,
52
+ onEventPress,
53
+ view,
54
+ });
55
+ logValidationErrors(errors);
56
+ }
57
+ }, [events, date, onDateChange, onEventPress, view]);
58
+
43
59
  // Merge user theme with default theme
44
60
  const mergedTheme = mergeTheme(theme);
45
61
 
@@ -155,7 +171,7 @@ export const CustomCalendar: React.FC<CalendarProps> = ({
155
171
  <View
156
172
  key={`month-${viewKey}`} // Force remount on view change
157
173
  style={[styles.container, { backgroundColor: mergedTheme.background }]}
158
- onLayout={(e) => {
174
+ onLayout={(e: LayoutChangeEvent) => {
159
175
  const newHeight = e.nativeEvent.layout.height;
160
176
  if (newHeight > 0) {
161
177
  setHeight(newHeight);
@@ -39,7 +39,7 @@ export const useScrollSynchronization = () => {
39
39
  * Runs on UI thread for zero-latency synchronization
40
40
  */
41
41
  const handleHeaderScroll = useAnimatedScrollHandler({
42
- onScroll: (event) => {
42
+ onScroll: (event: { contentOffset: { x: number; y: number } }) => {
43
43
  // Ignore if content is currently being scrolled by user
44
44
  if (isContentScrolling.value) {
45
45
  return;
@@ -69,7 +69,7 @@ export const useScrollSynchronization = () => {
69
69
  * Runs on UI thread for zero-latency synchronization
70
70
  */
71
71
  const handleContentScroll = useAnimatedScrollHandler({
72
- onScroll: (event) => {
72
+ onScroll: (event: { contentOffset: { x: number; y: number } }) => {
73
73
  // Ignore if header is currently being scrolled by user
74
74
  if (isHeaderScrolling.value) {
75
75
  return;
@@ -197,9 +197,7 @@ export const DayView: React.FC<DayViewProps> = ({
197
197
  <CalendarErrorBoundary
198
198
  theme={theme}
199
199
  onError={(error, errorInfo) => {
200
- if (process.env.NODE_ENV !== "production") {
201
- console.error("DayView Error:", error, errorInfo);
202
- }
200
+ // Error handled by ErrorBoundary's onError callback
203
201
  }}
204
202
  >
205
203
  <View style={[styles.container, { backgroundColor: theme.background }]}>
@@ -238,7 +236,7 @@ export const DayView: React.FC<DayViewProps> = ({
238
236
  showsVerticalScrollIndicator={true}
239
237
  showsHorizontalScrollIndicator={false}
240
238
  onScroll={useAnimatedScrollHandler({
241
- onScroll: (event) => {
239
+ onScroll: (event: { contentOffset: { y: number } }) => {
242
240
  scrollY.value = event.contentOffset.y;
243
241
  },
244
242
  })}
@@ -310,19 +308,21 @@ const createStyles = (theme: CalendarTheme) =>
310
308
  container: {
311
309
  flex: 1,
312
310
  // Web-specific: Ensure container has constrained height for scrolling
313
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
314
- height: '100%',
315
- minHeight: 0, // Important for flex children on web
316
- }),
311
+ ...(Platform.OS === "web" &&
312
+ typeof window !== "undefined" && {
313
+ height: "100%",
314
+ minHeight: 0, // Important for flex children on web
315
+ }),
317
316
  },
318
317
  contentContainer: {
319
318
  flex: 1,
320
319
  backgroundColor: theme.background,
321
320
  // Web-specific: Constrain height for scrolling
322
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
323
- height: '100%',
324
- minHeight: 0,
325
- }),
321
+ ...(Platform.OS === "web" &&
322
+ typeof window !== "undefined" && {
323
+ height: "100%",
324
+ minHeight: 0,
325
+ }),
326
326
  },
327
327
  stickyHeadersContainer: {
328
328
  flexDirection: "row",
@@ -336,9 +336,10 @@ const createStyles = (theme: CalendarTheme) =>
336
336
  stickyHeadersScroll: {
337
337
  flex: 1,
338
338
  // Web-specific: Ensure horizontal scrolling works
339
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
340
- minHeight: 0,
341
- }),
339
+ ...(Platform.OS === "web" &&
340
+ typeof window !== "undefined" && {
341
+ minHeight: 0,
342
+ }),
342
343
  },
343
344
  stickyHeadersRow: {
344
345
  flexDirection: "row",
@@ -347,15 +348,16 @@ const createStyles = (theme: CalendarTheme) =>
347
348
  flex: 1,
348
349
  paddingTop: CONTENT_PADDING_TOP,
349
350
  // Web-specific: Explicit height constraint and overflow for scrolling
350
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
351
- height: '100%',
352
- maxHeight: '100%',
353
- minHeight: 0,
354
- // @ts-ignore - web-specific CSS properties
355
- overflowY: 'auto',
356
- overflowX: 'hidden',
357
- WebkitOverflowScrolling: 'touch',
358
- }),
351
+ ...(Platform.OS === "web" &&
352
+ typeof window !== "undefined" && {
353
+ height: "100%",
354
+ maxHeight: "100%",
355
+ minHeight: 0,
356
+ // @ts-ignore - web-specific CSS properties
357
+ overflowY: "auto",
358
+ overflowX: "hidden",
359
+ WebkitOverflowScrolling: "touch",
360
+ }),
359
361
  },
360
362
  content: {
361
363
  flexDirection: "row",
@@ -368,13 +370,14 @@ const createStyles = (theme: CalendarTheme) =>
368
370
  propertyLanesScroll: {
369
371
  flex: 1,
370
372
  // Web-specific: Ensure horizontal scrolling works
371
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
372
- minHeight: 0,
373
- // @ts-ignore - web-specific CSS properties
374
- overflowX: 'auto',
375
- overflowY: 'hidden',
376
- WebkitOverflowScrolling: 'touch',
377
- }),
373
+ ...(Platform.OS === "web" &&
374
+ typeof window !== "undefined" && {
375
+ minHeight: 0,
376
+ // @ts-ignore - web-specific CSS properties
377
+ overflowX: "auto",
378
+ overflowY: "hidden",
379
+ WebkitOverflowScrolling: "touch",
380
+ }),
378
381
  },
379
382
  propertyLanesRow: {
380
383
  flexDirection: "row",
@@ -32,7 +32,7 @@ export const OverflowIndicator: React.FC<OverflowIndicatorProps> = ({
32
32
  // backgroundColor: `${theme.text}10`,
33
33
  zIndex: 200,
34
34
  }}
35
- onPress={(e) => {
35
+ onPress={(e: any) => {
36
36
  e.stopPropagation(); // Prevent day cell click
37
37
  onPress();
38
38
  }}
@@ -2,6 +2,7 @@ import calendarize from "calendarize";
2
2
  import dayjs from "dayjs";
3
3
  import * as React from "react";
4
4
  import { Text, TouchableOpacity, View, Dimensions } from "react-native";
5
+ import type { LayoutChangeEvent } from "react-native";
5
6
  import { CalendarEvent } from "../types";
6
7
  import { CalendarTheme } from "../utils/theme";
7
8
  import { useSwipeGesture } from "../hooks";
@@ -146,8 +147,8 @@ export const MonthView: React.FC<MonthViewProps> = ({
146
147
  // borderColor: theme.border,
147
148
  // borderRadius: 4,
148
149
  }}
149
- onLayout={({ nativeEvent: { layout } }) => {
150
- setCalendarWidth(layout.width);
150
+ onLayout={(e: LayoutChangeEvent) => {
151
+ setCalendarWidth(e.nativeEvent.layout.width);
151
152
  }}
152
153
  {...panResponder.panHandlers}
153
154
  >
@@ -1,4 +1,5 @@
1
1
  import dayjs from "dayjs";
2
+ import type { Dayjs } from "dayjs";
2
3
  import isBetween from "dayjs/plugin/isBetween";
3
4
  import { CalendarEvent } from "../types";
4
5
  import { EventPosition, HorizontalPosition } from "./types";
@@ -16,7 +17,7 @@ dayjs.extend(isBetween);
16
17
  */
17
18
  export function getHorizontalPositionInDay(
18
19
  event: CalendarEvent,
19
- date: dayjs.Dayjs,
20
+ date: Dayjs,
20
21
  cellWidth: number
21
22
  ): HorizontalPosition {
22
23
  const dayStart = date.startOf("day");
@@ -51,7 +52,7 @@ export function getHorizontalPositionInDay(
51
52
  */
52
53
  export function getMultiDayPosition(
53
54
  event: CalendarEvent,
54
- weekStartDate: dayjs.Dayjs,
55
+ weekStartDate: Dayjs,
55
56
  weekIndex: number,
56
57
  cellWidth: number
57
58
  ): EventPosition | null {
@@ -212,7 +213,7 @@ export function assignGlobalRows(
212
213
  export function calculateOverflowByDay(
213
214
  eventPositions: EventPosition[],
214
215
  weeks: number[][],
215
- target: dayjs.Dayjs,
216
+ target: Dayjs,
216
217
  maxVisibleRows: number
217
218
  ): Map<string, number> {
218
219
  const overflowMap = new Map<string, number>();
@@ -33,13 +33,13 @@ export const WeekOverflowIndicator: React.FC<Props> = ({
33
33
  }) => {
34
34
  const label = showCount && count > 0 ? `+${count}` : undefined;
35
35
  // Use Unicode chevrons instead of icon library for web compatibility
36
- const chevronSymbol = chevron === "up" ? "▲" : chevron === "down" ? "▼" : null;
36
+ const chevronSymbol =
37
+ chevron === "up" ? "▲" : chevron === "down" ? "▼" : null;
37
38
 
38
39
  return (
39
40
  <TouchableOpacity
40
41
  style={[styles.container, style]}
41
42
  onPress={() => {
42
- console.log("WeekOverflowIndicator pressed");
43
43
  onPress && onPress();
44
44
  }}
45
45
  hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
@@ -73,7 +73,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
73
73
  }) => {
74
74
  // SSR-safe: useWindowDimensions may not be available during SSR
75
75
  const windowDimensions = useWindowDimensions();
76
- const width = typeof window !== 'undefined' ? windowDimensions.width : 0;
76
+ const width = typeof window !== "undefined" ? windowDimensions.width : 0;
77
77
  const dayColumnWidth = Math.floor((width - TIME_COLUMN_WIDTH) / 7);
78
78
 
79
79
  const totalHeight = HOURS_IN_DAY * WEEK_VIEW_HOUR_HEIGHT;
@@ -184,9 +184,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
184
184
  <CalendarErrorBoundary
185
185
  theme={theme}
186
186
  onError={(error, errorInfo) => {
187
- if (process.env.NODE_ENV !== "production") {
188
- console.error("WeekView Error:", error, errorInfo);
189
- }
187
+ // Error handled by ErrorBoundary's onError callback
190
188
  }}
191
189
  >
192
190
  <View style={styles.container}>
@@ -237,7 +235,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
237
235
  showsVerticalScrollIndicator
238
236
  showsHorizontalScrollIndicator={false}
239
237
  onScroll={useAnimatedScrollHandler({
240
- onScroll: (event) => {
238
+ onScroll: (event: { contentOffset: { y: number } }) => {
241
239
  scrollY.value = event.contentOffset.y;
242
240
  },
243
241
  })}
@@ -339,10 +337,11 @@ const createStyles = (theme: CalendarTheme, totalHeight: number) =>
339
337
  backgroundColor: theme.background,
340
338
  position: "relative",
341
339
  // Web-specific: Ensure container has constrained height for scrolling
342
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
343
- height: '100%',
344
- minHeight: 0, // Important for flex children on web
345
- }),
340
+ ...(Platform.OS === "web" &&
341
+ typeof window !== "undefined" && {
342
+ height: "100%",
343
+ minHeight: 0, // Important for flex children on web
344
+ }),
346
345
  },
347
346
  todayHighlight: {
348
347
  position: "absolute",
@@ -367,15 +366,16 @@ const createStyles = (theme: CalendarTheme, totalHeight: number) =>
367
366
  flex: 1,
368
367
  paddingTop: DAY_OVERFLOW_TOP_INSET,
369
368
  // Web-specific: Explicit height constraint and overflow for scrolling
370
- ...(Platform.OS === 'web' && typeof window !== 'undefined' && {
371
- height: '100%',
372
- maxHeight: '100%',
373
- minHeight: 0,
374
- // @ts-ignore - web-specific CSS properties
375
- overflowY: 'auto',
376
- overflowX: 'hidden',
377
- WebkitOverflowScrolling: 'touch',
378
- }),
369
+ ...(Platform.OS === "web" &&
370
+ typeof window !== "undefined" && {
371
+ height: "100%",
372
+ maxHeight: "100%",
373
+ minHeight: 0,
374
+ // @ts-ignore - web-specific CSS properties
375
+ overflowY: "auto",
376
+ overflowX: "hidden",
377
+ WebkitOverflowScrolling: "touch",
378
+ }),
379
379
  },
380
380
  content: {
381
381
  flexDirection: "row",
@@ -1,5 +1,5 @@
1
1
  import { useRef, useMemo } from "react";
2
- import { PanResponder, PanResponderInstance, Platform } from "react-native";
2
+ import { PanResponder, PanResponderInstance, PanResponderGestureState, Platform } from "react-native";
3
3
 
4
4
  /**
5
5
  * Configuration options for swipe gesture detection
@@ -76,7 +76,7 @@ export function useSwipeGesture(
76
76
  * Only respond to horizontal gestures (where horizontal movement > vertical movement).
77
77
  * See: https://stackoverflow.com/questions/47568850/touchableopacity-with-parent-panresponder
78
78
  */
79
- onMoveShouldSetPanResponder: (_, gestureState) => {
79
+ onMoveShouldSetPanResponder: (_: any, gestureState: PanResponderGestureState) => {
80
80
  if (!enabled) {
81
81
  return false;
82
82
  }
@@ -97,7 +97,7 @@ export function useSwipeGesture(
97
97
  * Handle the gesture movement.
98
98
  * Trigger callbacks when threshold is exceeded.
99
99
  */
100
- onPanResponderMove: (_, gestureState) => {
100
+ onPanResponderMove: (_: any, gestureState: PanResponderGestureState) => {
101
101
  const { dx, dy } = gestureState;
102
102
 
103
103
  // Ignore if:
@@ -38,9 +38,6 @@ export class CalendarErrorBoundary extends Component<
38
38
  }
39
39
 
40
40
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
41
- if (process.env.NODE_ENV !== "production") {
42
- console.error("Calendar Error:", error, errorInfo);
43
- }
44
41
  this.props.onError?.(error, errorInfo);
45
42
  }
46
43
 
Binary file
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Type declarations for dayjs
3
+ * dayjs is a peer dependency, so types are provided here for type checking
4
+ */
5
+ declare module 'dayjs' {
6
+ export interface Dayjs {
7
+ format(template?: string): string;
8
+ startOf(unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
9
+ endOf(unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
10
+ add(value: number, unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
11
+ subtract(value: number, unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
12
+ toDate(): Date;
13
+ valueOf(): number;
14
+ [key: string]: any;
15
+ }
16
+
17
+ interface DayjsExtend {
18
+ extend(plugin: any, option?: any): void;
19
+ }
20
+
21
+ const dayjs: ((date?: Date | string | number) => Dayjs) & DayjsExtend;
22
+
23
+ export default dayjs;
24
+ }
25
+
@@ -26,8 +26,30 @@ declare module 'react-native' {
26
26
  export const PanResponder: any;
27
27
 
28
28
  export type PanResponderInstance = any;
29
+ export type PanResponderGestureState = {
30
+ dx: number;
31
+ dy: number;
32
+ moveX: number;
33
+ moveY: number;
34
+ numberActiveTouches: number;
35
+ stateID: number;
36
+ vx: number;
37
+ vy: number;
38
+ x0: number;
39
+ y0: number;
40
+ };
29
41
  export type NativeSyntheticEvent<T> = any;
30
42
  export type NativeTouchEvent = any;
31
43
  export type StyleProp<T> = any;
44
+ export type LayoutChangeEvent = {
45
+ nativeEvent: {
46
+ layout: {
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ };
52
+ };
53
+ };
32
54
  }
33
55
 
@@ -20,6 +20,9 @@ export * from "./eventHelpers";
20
20
  // Property colors
21
21
  export * from "./propertyColors";
22
22
 
23
+ // Validation utilities
24
+ export * from "./validation";
25
+
23
26
  // Platform detection utilities
24
27
  export * from "./platform";
25
28
 
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Prop validation utilities for Calendar component
3
+ * Provides helpful error messages for common mistakes
4
+ */
5
+
6
+ import { CalendarEvent, ViewMode } from "../types";
7
+
8
+ export interface ValidationError {
9
+ field: string;
10
+ message: string;
11
+ index?: number;
12
+ }
13
+
14
+ /**
15
+ * Validate a CalendarEvent object
16
+ * @param event - The event to validate
17
+ * @param index - Index of the event in the array (for error messages)
18
+ * @returns Array of validation errors (empty if valid)
19
+ */
20
+ export function validateEvent(
21
+ event: CalendarEvent,
22
+ index: number
23
+ ): ValidationError[] {
24
+ const errors: ValidationError[] = [];
25
+
26
+ // Check required fields
27
+ if (!event.id || typeof event.id !== "string") {
28
+ errors.push({
29
+ field: "id",
30
+ message: `Event at index ${index}: "id" is required and must be a string`,
31
+ index,
32
+ });
33
+ }
34
+
35
+ if (!event.eventId || typeof event.eventId !== "string") {
36
+ errors.push({
37
+ field: "eventId",
38
+ message: `Event at index ${index}: "eventId" is required and must be a string`,
39
+ index,
40
+ });
41
+ }
42
+
43
+ if (!event.title || typeof event.title !== "string") {
44
+ errors.push({
45
+ field: "title",
46
+ message: `Event at index ${index}: "title" is required and must be a string`,
47
+ index,
48
+ });
49
+ }
50
+
51
+ // Check start date
52
+ if (!event.start) {
53
+ errors.push({
54
+ field: "start",
55
+ message: `Event at index ${index}: "start" is required`,
56
+ index,
57
+ });
58
+ } else if (!(event.start instanceof Date)) {
59
+ errors.push({
60
+ field: "start",
61
+ message: `Event at index ${index}: "start" must be a Date object, got ${typeof event.start}. Use: new Date(dateString) to convert.`,
62
+ index,
63
+ });
64
+ } else if (isNaN(event.start.getTime())) {
65
+ errors.push({
66
+ field: "start",
67
+ message: `Event at index ${index}: "start" is an invalid Date`,
68
+ index,
69
+ });
70
+ }
71
+
72
+ // Check end date
73
+ if (!event.end) {
74
+ errors.push({
75
+ field: "end",
76
+ message: `Event at index ${index}: "end" is required`,
77
+ index,
78
+ });
79
+ } else if (!(event.end instanceof Date)) {
80
+ errors.push({
81
+ field: "end",
82
+ message: `Event at index ${index}: "end" must be a Date object, got ${typeof event.end}. Use: new Date(dateString) to convert.`,
83
+ index,
84
+ });
85
+ } else if (isNaN(event.end.getTime())) {
86
+ errors.push({
87
+ field: "end",
88
+ message: `Event at index ${index}: "end" is an invalid Date`,
89
+ index,
90
+ });
91
+ }
92
+
93
+ // Check date range validity
94
+ if (
95
+ event.start instanceof Date &&
96
+ event.end instanceof Date &&
97
+ !isNaN(event.start.getTime()) &&
98
+ !isNaN(event.end.getTime())
99
+ ) {
100
+ if (event.start >= event.end) {
101
+ errors.push({
102
+ field: "dateRange",
103
+ message: `Event at index ${index}: "start" date must be before "end" date`,
104
+ index,
105
+ });
106
+ }
107
+ }
108
+
109
+ return errors;
110
+ }
111
+
112
+ /**
113
+ * Validate Calendar component props
114
+ * @param props - The props to validate
115
+ * @returns Array of validation errors (empty if valid)
116
+ */
117
+ export function validateCalendarProps(props: {
118
+ events: CalendarEvent[];
119
+ date: Date;
120
+ onDateChange?: (date: Date) => void;
121
+ onEventPress?: (event: CalendarEvent) => void;
122
+ view?: ViewMode;
123
+ }): ValidationError[] {
124
+ const errors: ValidationError[] = [];
125
+
126
+ // Validate date prop
127
+ if (!props.date) {
128
+ errors.push({
129
+ field: "date",
130
+ message: '"date" prop is required',
131
+ });
132
+ } else if (!(props.date instanceof Date)) {
133
+ errors.push({
134
+ field: "date",
135
+ message: `"date" must be a Date object, got ${typeof props.date}. Use: new Date() or new Date(dateString)`,
136
+ });
137
+ } else if (isNaN(props.date.getTime())) {
138
+ errors.push({
139
+ field: "date",
140
+ message: '"date" is an invalid Date',
141
+ });
142
+ }
143
+
144
+ // Validate required callbacks
145
+ if (!props.onDateChange || typeof props.onDateChange !== "function") {
146
+ errors.push({
147
+ field: "onDateChange",
148
+ message: '"onDateChange" prop is required and must be a function',
149
+ });
150
+ }
151
+
152
+ if (!props.onEventPress || typeof props.onEventPress !== "function") {
153
+ errors.push({
154
+ field: "onEventPress",
155
+ message: '"onEventPress" prop is required and must be a function',
156
+ });
157
+ }
158
+
159
+ // Validate view prop
160
+ if (props.view && !["month", "week", "day"].includes(props.view)) {
161
+ errors.push({
162
+ field: "view",
163
+ message: `"view" must be one of: "month", "week", "day". Got: ${props.view}`,
164
+ });
165
+ }
166
+
167
+ // Validate events array
168
+ if (!Array.isArray(props.events)) {
169
+ errors.push({
170
+ field: "events",
171
+ message: '"events" must be an array',
172
+ });
173
+ } else {
174
+ // Validate each event
175
+ props.events.forEach((event, index) => {
176
+ const eventErrors = validateEvent(event, index);
177
+ errors.push(...eventErrors);
178
+ });
179
+ }
180
+
181
+ return errors;
182
+ }
183
+
184
+ /**
185
+ * Log validation errors in development mode
186
+ * @param errors - Array of validation errors
187
+ */
188
+ export function logValidationErrors(errors: ValidationError[]): void {
189
+ if (errors.length === 0) return;
190
+
191
+ if (process.env.NODE_ENV !== "production") {
192
+ console.error(
193
+ "❌ Calendar Component Validation Errors:\n" +
194
+ errors.map((err) => ` • ${err.message}`).join("\n") +
195
+ "\n\n" +
196
+ "Common fixes:\n" +
197
+ " • Convert date strings to Date objects: new Date(dateString)\n" +
198
+ " • Ensure start < end for all events\n" +
199
+ " • Provide required callbacks: onDateChange, onEventPress"
200
+ );
201
+ }
202
+ }
203
+
package/tsup.config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineConfig } from "tsup";
2
2
 
3
3
  export default defineConfig({
4
- entry: ["index.ts"],
4
+ entry: ["src/index.ts"],
5
5
  format: ["cjs", "esm"],
6
6
  dts: true,
7
7
  sourcemap: true,