react-native-resource-calendar 1.0.19 β†’ 1.0.21

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
@@ -10,9 +10,6 @@ Expo compatibility.
10
10
  - βœ… Multi-resource/multi-days timeline layout
11
11
  - 🎨 Customizable event slots (Body, TopRight)
12
12
  - πŸ“± Smooth Reanimated drag-and-drop
13
- - ⏰ Timezone-aware time labels
14
- - 🧩 Modular store (Zustand binding by default)
15
- - πŸ–ŒοΈ Theme support (font families, weights)
16
13
  - πŸͺΆ Lightweight and Expo-ready
17
14
 
18
15
  ---
@@ -42,8 +39,7 @@ react-native-gesture-handler \
42
39
  react-native-reanimated \
43
40
  react-native-svg \
44
41
  @shopify/flash-list \
45
- @shopify/react-native-skia \
46
- expo-haptics
42
+ @shopify/react-native-skia
47
43
  ```
48
44
 
49
45
  If you’re using bare React Native (not Expo), install them manually:
@@ -54,8 +50,19 @@ react-native-gesture-handler \
54
50
  react-native-reanimated \
55
51
  react-native-svg \
56
52
  @shopify/flash-list \
57
- @shopify/react-native-skia \
58
- expo-haptics
53
+ @shopify/react-native-skia
54
+ ```
55
+
56
+ 🟦 Optional: Haptics Support (Expo Only)
57
+
58
+ Haptic feedback is optional.
59
+ If you want to enable vibration feedback when interacting with components, install the Expo Haptics package and set
60
+ enableHapticFeedback to true in your component config.
61
+
62
+ πŸ“¦ Install (Expo)
63
+
64
+ ```bash
65
+ npx expo install expo-haptics
59
66
  ```
60
67
 
61
68
  ---
@@ -251,28 +258,30 @@ export default function App() {
251
258
 
252
259
  The `Calendar` component accepts a flexible set of props for customizing layout, theme, and interactivity.
253
260
 
254
- | Prop | Type | Default | Description |
255
- |-----------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
256
- | **`date`** | `Date` | `new Date()` | The anchor day shown in the timeline. In multi-day modes this is the **first** visible day. |
257
- | **`mode`** | `CalendarMode` (`'day' \| '3days' \| 'week'`) | `'day'` | Controls the column semantics. **day** = many resources for one day. **3days/week** = several days for one resource. |
258
- | **`activeResourceId`** | `number` | first `resources[0].id` | When `mode !== 'day'`, columns represent days for **this** resource. |
259
- | **`resources`** | `Array<Resource & { events: Event[]; disabledBlocks?: DisabledBlock[]; disableIntervals?: DisabledInterval[] }>` | **required** | Resource columns. Each resource includes its day’s `events`, optional `disabledBlocks` (finite ranges), and `disableIntervals` (open/recurring ranges). |
260
- | **`timezone`** | `string` | device time zone | Used for time labels and converting block taps to a `Date`. |
261
- | **`startMinutes`** | `number` | `0` | Start of visible timeline in minutes after midnight (e.g. `8 * 60` = 08:00). |
262
- | **`numberOfColumns`** | `number` | `3` | **Day mode only.** How many resource columns to show side-by-side. (In multi-day modes, the column count is fixed by the mode: 3 or 7.) |
263
- | **`hourHeight`** | `number` | `120` | Vertical density, px per hour. Affects drag/resize and scroll snap. |
264
- | **`snapIntervalInMinutes`** | `number` | `5` | Drag/resize snapping granularity (in minutes). |
265
- | **`overLappingLayoutMode`** | `LayoutMode` (`'stacked' \| 'columns'`) | `'stacked'` | Strategy to lay out overlapping events inside a column. |
266
- | **`theme`** | `CalendarTheme` | β€” | Typography & palette overrides. |
267
- | **`eventSlots`** | `EventSlots` | β€” | Slot renderers to customize event content (e.g. `{ Body, TopRight }`). |
268
- | **`eventStyleOverrides`** | `StyleOverrides \| ((event: Event) => StyleOverrides \| undefined)` | β€” | Per-event style override (object or function). |
269
- | **`isEventSelected`** | `(event: Event) => boolean` | `() => false` | Marks which events are currently selected. |
270
- | **`isEventDisabled`** | `(event: Event) => boolean` | `() => false` | Marks events as disabled (non-interactive). |
271
- | **`onResourcePress`** | `(resource: Resource) => void` | β€” | Invoked when a resource header is pressed. |
272
- | **`onBlockLongPress`** | `(resource: Resource, date: Date) => void` | β€” | Long-press on an empty block (grid). |
273
- | **`onDisabledBlockPress`** | `(block: DisabledBlock) => void` | β€” | Tap on a disabled block (e.g., lunch). |
274
- | **`onEventPress`** | `(event: Event) => void` | β€” | Tap on an event. |
275
- | **`onEventLongPress`** | `(event: Event) => void` | β€” | Long-press on an event. The calendar also preps internal drag state here. |
261
+ | Prop | Type | Default | Description |
262
+ |-----------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
263
+ | **`date`** | `Date` | `new Date()` | The anchor day shown in the timeline. In multi-day modes this is the **first** visible day. |
264
+ | **`mode`** | `CalendarMode` (`'day' \| '3days' \| 'week'`) | `'day'` | Controls the column semantics. **day** = many resources for one day. **3days/week** = several days for one resource. |
265
+ | **`activeResourceId`** | `number` | first `resources[0].id` | When `mode !== 'day'`, columns represent days for **this** resource. |
266
+ | **`resources`** | `Array<Resource & { events: Event[]; disabledBlocks?: DisabledBlock[]; disableIntervals?: DisabledInterval[] }>` | **required** | Resource columns. Each resource includes its day’s `events`, optional `disabledBlocks`, and `disableIntervals`. |
267
+ | **`timezone`** | `string` | device time zone | Used for time labels and converting block taps to a `Date`. |
268
+ | **`startMinutes`** | `number` | `0` | Start of visible timeline in minutes after midnight (e.g. `8 * 60` = 08:00). |
269
+ | **`numberOfColumns`** | `number` | `3` | **Day mode only.** How many resource columns to show side-by-side. (In multi-day modes, the column count is fixed by the mode: 3 or 7.) |
270
+ | **`hourHeight`** | `number` | `120` | Vertical density, px per hour. Affects drag/resize and scroll snap. |
271
+ | **`snapIntervalInMinutes`** | `number` | `5` | Drag/resize snapping granularity (in minutes). |
272
+ | **`overLappingLayoutMode`** | `LayoutMode` (`'stacked' \| 'columns'`) | `'stacked'` | Strategy to lay out overlapping events inside a column. |
273
+ | **`theme`** | `CalendarTheme` | β€” | Typography & palette overrides. |
274
+ | **`enableHapticFeedback`** | `boolean` | `false` | Enable haptic feedback. |
275
+ | **`eventSlots`** | `EventSlots` | β€” | Slot renderers to customize event content (e.g. `{ Body, TopRight }`). |
276
+ | **`eventStyleOverrides`** | `StyleOverrides \| ((event: Event) => StyleOverrides \| undefined)` | β€” | Per-event style override (object or function). |
277
+ | **`isEventSelected`** | `(event: Event) => boolean` | `() => false` | Marks which events are currently selected. |
278
+ | **`isEventDisabled`** | `(event: Event) => boolean` | `() => false` | Marks events as disabled (non-interactive). |
279
+ | **`onResourcePress`** | `(resource: Resource) => void` | β€” | Invoked when a resource header is pressed. |
280
+ | **`onBlockLongPress`** | `(resource: Resource, date: Date) => void` | β€” | Long-press on an empty block (grid). |
281
+ | **`onBlockTap`** | `(resource: Resource, date: Date) => void` | β€” | Tap on an empty block (grid). |
282
+ | **`onDisabledBlockPress`** | `(block: DisabledBlock) => void` | β€” | Tap on a disabled block (e.g., lunch). |
283
+ | **`onEventPress`** | `(event: Event) => void` | β€” | Tap on an event. |
284
+ | **`onEventLongPress`** | `(event: Event) => void` | β€” | Long-press on an event. The calendar also preps internal drag state here. |
276
285
 
277
286
  ---
278
287
 
@@ -329,7 +338,7 @@ type CalendarTheme = {
329
338
  fontFamily?: string;
330
339
  };
331
340
  };
332
-
341
+
333
342
  type CalendarMode = 'day' | '3days' | 'week';
334
343
  ```
335
344
 
package/dist/index.d.mts CHANGED
@@ -86,9 +86,11 @@ interface CalendarProps {
86
86
  hourHeight?: number;
87
87
  onResourcePress?: (resource: Resource) => void;
88
88
  onBlockLongPress?: (resource: Resource, date: Date) => void;
89
+ onBlockTap?: (resource: Resource, date: Date) => void;
89
90
  onDisabledBlockPress?: (block: DisabledBlock) => void;
90
91
  onEventPress?: (event: Event) => void;
91
92
  onEventLongPress?: (event: Event) => void;
93
+ enableHapticFeedback?: boolean;
92
94
  eventSlots?: EventSlots;
93
95
  eventStyleOverrides?: StyleOverrides | ((event: Event) => StyleOverrides | undefined);
94
96
  isEventSelected?: FlagFn;
package/dist/index.d.ts CHANGED
@@ -86,9 +86,11 @@ interface CalendarProps {
86
86
  hourHeight?: number;
87
87
  onResourcePress?: (resource: Resource) => void;
88
88
  onBlockLongPress?: (resource: Resource, date: Date) => void;
89
+ onBlockTap?: (resource: Resource, date: Date) => void;
89
90
  onDisabledBlockPress?: (block: DisabledBlock) => void;
90
91
  onEventPress?: (event: Event) => void;
91
92
  onEventLongPress?: (event: Event) => void;
93
+ enableHapticFeedback?: boolean;
92
94
  eventSlots?: EventSlots;
93
95
  eventStyleOverrides?: StyleOverrides | ((event: Event) => StyleOverrides | undefined);
94
96
  isEventSelected?: FlagFn;
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ var reactNativeGestureHandler = require('react-native-gesture-handler');
5
5
  var Animated2 = require('react-native-reanimated');
6
6
  var reactNative = require('react-native');
7
7
  var flashList = require('@shopify/flash-list');
8
- var Haptics = require('expo-haptics');
9
8
  var dateFnsTz = require('date-fns-tz');
10
9
  var dateFns = require('date-fns');
11
10
  var lodash = require('lodash');
@@ -39,7 +38,6 @@ function _interopNamespace(e) {
39
38
 
40
39
  var React19__namespace = /*#__PURE__*/_interopNamespace(React19);
41
40
  var Animated2__default = /*#__PURE__*/_interopDefault(Animated2);
42
- var Haptics__namespace = /*#__PURE__*/_interopNamespace(Haptics);
43
41
  var Svg__default = /*#__PURE__*/_interopDefault(Svg);
44
42
 
45
43
  // src/components/Calendar.tsx
@@ -747,6 +745,7 @@ function StaffAvatar({
747
745
  }
748
746
  var EventGridBlocksSkia = ({
749
747
  handleBlockPress,
748
+ handleBlockLongPress,
750
749
  hourHeight,
751
750
  APPOINTMENT_BLOCK_WIDTH
752
751
  }) => {
@@ -788,6 +787,16 @@ var EventGridBlocksSkia = ({
788
787
  },
789
788
  [handleBlockPress, timeLabels]
790
789
  );
790
+ const onSlotLongPress = React19__namespace.useCallback(
791
+ (row) => {
792
+ setPressedRow(null);
793
+ const slot = timeLabels[row];
794
+ if (slot) {
795
+ handleBlockLongPress(slot);
796
+ }
797
+ },
798
+ [timeLabels, handleBlockLongPress]
799
+ );
791
800
  const onPressBegin = React19__namespace.useCallback((row) => {
792
801
  setPressedRow(row);
793
802
  }, []);
@@ -797,14 +806,31 @@ var EventGridBlocksSkia = ({
797
806
  const longPressGesture = reactNativeGestureHandler.Gesture.LongPress().onBegin((e) => {
798
807
  "worklet";
799
808
  Animated2.runOnJS(onPressBegin)(Math.floor(e.y / rowHeight));
809
+ }).onTouchesUp(() => {
810
+ "worklet";
811
+ Animated2.runOnJS(onTouchesUp)();
812
+ }).onEnd((e) => {
813
+ "worklet";
814
+ Animated2.runOnJS(onSlotLongPress)(Math.floor(e.y / rowHeight));
815
+ }).onFinalize(() => {
816
+ "worklet";
817
+ Animated2.runOnJS(onTouchesUp)();
818
+ });
819
+ const tapGesture = reactNativeGestureHandler.Gesture.Tap().onBegin((e) => {
820
+ "worklet";
821
+ Animated2.runOnJS(onPressBegin)(Math.floor(e.y / rowHeight));
800
822
  }).onEnd((e) => {
801
823
  "worklet";
802
824
  Animated2.runOnJS(onSlotPress)(Math.floor(e.y / rowHeight));
825
+ }).onTouchesUp(() => {
826
+ "worklet";
827
+ Animated2.runOnJS(onTouchesUp)();
803
828
  }).onFinalize(() => {
804
829
  "worklet";
805
830
  Animated2.runOnJS(onTouchesUp)();
806
831
  });
807
- return /* @__PURE__ */ React19__namespace.createElement(reactNativeGestureHandler.GestureDetector, { gesture: longPressGesture }, /* @__PURE__ */ React19__namespace.createElement(reactNative.View, null, /* @__PURE__ */ React19__namespace.createElement(reactNativeSkia.Canvas, { style: { width: APPOINTMENT_BLOCK_WIDTH, height: segmentHeight } }, firstRects.map(({ x, y, width: w, height: h, row }, idx) => /* @__PURE__ */ React19__namespace.createElement(React19__namespace.Fragment, { key: idx }, /* @__PURE__ */ React19__namespace.createElement(
832
+ const composedGesture = reactNativeGestureHandler.Gesture.Race(longPressGesture, tapGesture);
833
+ return /* @__PURE__ */ React19__namespace.createElement(reactNativeGestureHandler.GestureDetector, { gesture: composedGesture }, /* @__PURE__ */ React19__namespace.createElement(reactNative.View, null, /* @__PURE__ */ React19__namespace.createElement(reactNativeSkia.Canvas, { style: { width: APPOINTMENT_BLOCK_WIDTH, height: segmentHeight } }, firstRects.map(({ x, y, width: w, height: h, row }, idx) => /* @__PURE__ */ React19__namespace.createElement(React19__namespace.Fragment, { key: idx }, /* @__PURE__ */ React19__namespace.createElement(
808
834
  reactNativeSkia.Rect,
809
835
  {
810
836
  x,
@@ -1053,7 +1079,8 @@ var EventBlock = React19__namespace.default.memo(({
1053
1079
  fontSize: getTextSize(hourHeight),
1054
1080
  pointerEvents: "none",
1055
1081
  padding: 0,
1056
- margin: 0
1082
+ margin: 0,
1083
+ color: "black"
1057
1084
  }, resolved?.time],
1058
1085
  defaultValue: `${start} - ${end}`
1059
1086
  }
@@ -1172,7 +1199,8 @@ var DraggableEvent = ({
1172
1199
  fontSize: getTextSize(hourHeight),
1173
1200
  pointerEvents: "none",
1174
1201
  padding: 0,
1175
- margin: 0
1202
+ margin: 0,
1203
+ color: "black"
1176
1204
  }, resolved?.time],
1177
1205
  defaultValue: initialDisplayTime,
1178
1206
  animatedProps: animatedTimeProps
@@ -1346,9 +1374,11 @@ var CalendarInner = (props) => {
1346
1374
  resources,
1347
1375
  onResourcePress,
1348
1376
  onBlockLongPress,
1377
+ onBlockTap,
1349
1378
  onEventPress,
1350
1379
  onEventLongPress,
1351
1380
  onDisabledBlockPress,
1381
+ enableHapticFeedback = false,
1352
1382
  eventSlots,
1353
1383
  eventStyleOverrides,
1354
1384
  overLappingLayoutMode = "stacked",
@@ -1465,9 +1495,19 @@ var CalendarInner = (props) => {
1465
1495
  const startedX = Animated2.useSharedValue(0);
1466
1496
  const startedY = Animated2.useSharedValue(0);
1467
1497
  const touchY = Animated2.useSharedValue(0);
1468
- const triggerHaptic = React19.useCallback(() => {
1469
- Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Light);
1470
- }, []);
1498
+ const triggerHaptic = React19.useCallback(
1499
+ async (style = "Light") => {
1500
+ try {
1501
+ const Haptics = await import('expo-haptics');
1502
+ const feedbackStyle = Haptics.ImpactFeedbackStyle[style];
1503
+ if (enableHapticFeedback)
1504
+ await Haptics.impactAsync(feedbackStyle);
1505
+ } catch (e) {
1506
+ console.log("Haptics not available, skipping...");
1507
+ }
1508
+ },
1509
+ [enableHapticFeedback]
1510
+ );
1471
1511
  const resourceIds = React19.useMemo(() => {
1472
1512
  const ids = resources?.map((item) => item?.id) || [];
1473
1513
  if (JSON.stringify(prevResourceIdsRef.current) !== JSON.stringify(ids)) {
@@ -1628,7 +1668,7 @@ var CalendarInner = (props) => {
1628
1668
  const increment = APPOINTMENT_BLOCK_WIDTH * Math.sign(autoScrollXSpeed.value);
1629
1669
  const newScrollX = scrollX.value + increment;
1630
1670
  Animated2.runOnJS(scrollListTo)(newScrollX);
1631
- Animated2.runOnJS(Haptics__namespace.impactAsync)(Haptics__namespace.ImpactFeedbackStyle.Medium);
1671
+ Animated2.runOnJS(triggerHaptic)("Medium");
1632
1672
  }
1633
1673
  });
1634
1674
  Animated2.useFrameCallback(() => {
@@ -1662,7 +1702,7 @@ var CalendarInner = (props) => {
1662
1702
  const scrollDiff = Math.abs(newScrollY - lastHapticScrollY.value);
1663
1703
  if (scrollDiff >= snapInterval) {
1664
1704
  lastHapticScrollY.value = newScrollY;
1665
- Animated2.runOnJS(Haptics__namespace.impactAsync)(Haptics__namespace.ImpactFeedbackStyle.Medium);
1705
+ Animated2.runOnJS(triggerHaptic)("Medium");
1666
1706
  }
1667
1707
  });
1668
1708
  React19.useEffect(() => {
@@ -1696,7 +1736,7 @@ var CalendarInner = (props) => {
1696
1736
  eventHeight.value = initialHeight;
1697
1737
  setSelectedEvent(event);
1698
1738
  requestAnimationFrame(() => setDragReady(true));
1699
- Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Medium);
1739
+ Animated2.runOnJS(triggerHaptic)("Medium");
1700
1740
  };
1701
1741
  }, []);
1702
1742
  const internalStableOnLongPress = React19.useCallback((e) => {
@@ -1719,12 +1759,18 @@ var CalendarInner = (props) => {
1719
1759
  }
1720
1760
  }
1721
1761
  });
1722
- const handleBlockPress = React19.useCallback((resourceId, time) => {
1723
- Haptics__namespace.impactAsync(Haptics__namespace.ImpactFeedbackStyle.Medium);
1762
+ const handleBlockLongPress = React19.useCallback((resourceId, time) => {
1763
+ Animated2.runOnJS(triggerHaptic)("Medium");
1724
1764
  const resource = resources.find((r) => r.id === resourceId);
1725
1765
  if (onBlockLongPress)
1726
1766
  onBlockLongPress(resource, new Date(time));
1727
1767
  }, [resources, onBlockLongPress]);
1768
+ const handleBlockPress = React19.useCallback((resourceId, time) => {
1769
+ Animated2.runOnJS(triggerHaptic)("Medium");
1770
+ const resource = resources.find((r) => r.id === resourceId);
1771
+ if (onBlockTap)
1772
+ onBlockTap(resource, new Date(time));
1773
+ }, [resources, onBlockTap]);
1728
1774
  React19.useEffect(() => {
1729
1775
  const handleOrientationChange = () => {
1730
1776
  if (selectedEvent) {
@@ -1748,7 +1794,8 @@ var CalendarInner = (props) => {
1748
1794
  {
1749
1795
  hourHeight,
1750
1796
  APPOINTMENT_BLOCK_WIDTH,
1751
- handleBlockPress: (time) => handleBlockPress(rid, combineDateAndTime(dayDate ?? dateRef.current, time))
1797
+ handleBlockPress: (time) => handleBlockPress(rid, combineDateAndTime(dayDate ?? dateRef.current, time)),
1798
+ handleBlockLongPress: (time) => handleBlockLongPress(rid, combineDateAndTime(dayDate ?? dateRef.current, time))
1752
1799
  }
1753
1800
  ), /* @__PURE__ */ React19__namespace.default.createElement(
1754
1801
  DisabledIntervals_default,