@dreamstack-us/kaal 0.0.3 → 0.0.5

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.
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom" />
2
2
  import type React from 'react';
3
3
  import { memo, useMemo } from 'react';
4
- import { Pressable, StyleSheet, Text } from 'react-native';
4
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
5
5
  import { useDatePickerOverrides } from '../../context/ThemeOverrideContext';
6
6
 
7
7
  interface DayCellProps {
@@ -31,17 +31,52 @@ const DEFAULT_COLORS = {
31
31
  cellBorderRadius: 22,
32
32
  };
33
33
 
34
+ const CELL_SIZE = 44;
35
+ const BAND_HEIGHT = 28; // Narrower band for thermometer effect
36
+
34
37
  // Web-compatible styles (no unistyles dependency)
35
38
  const webStyles = StyleSheet.create({
36
39
  cell: {
37
- width: 44,
38
- height: 44,
40
+ width: CELL_SIZE,
41
+ height: CELL_SIZE,
39
42
  justifyContent: 'center',
40
43
  alignItems: 'center',
41
44
  },
45
+ // Thermometer band for in-range dates
46
+ rangeBand: {
47
+ position: 'absolute',
48
+ top: (CELL_SIZE - BAND_HEIGHT) / 2,
49
+ height: BAND_HEIGHT,
50
+ left: 0,
51
+ right: 0,
52
+ },
53
+ // Half band extending right from range start
54
+ rangeBandRight: {
55
+ position: 'absolute',
56
+ top: (CELL_SIZE - BAND_HEIGHT) / 2,
57
+ height: BAND_HEIGHT,
58
+ left: CELL_SIZE / 2,
59
+ right: 0,
60
+ },
61
+ // Half band extending left to range end
62
+ rangeBandLeft: {
63
+ position: 'absolute',
64
+ top: (CELL_SIZE - BAND_HEIGHT) / 2,
65
+ height: BAND_HEIGHT,
66
+ left: 0,
67
+ right: CELL_SIZE / 2,
68
+ },
69
+ // Circle overlay for selected dates
70
+ circleOverlay: {
71
+ position: 'absolute',
72
+ width: CELL_SIZE,
73
+ height: CELL_SIZE,
74
+ borderRadius: CELL_SIZE / 2,
75
+ },
42
76
  text: {
43
77
  fontSize: 17,
44
78
  fontWeight: '400',
79
+ zIndex: 1,
45
80
  },
46
81
  });
47
82
 
@@ -59,48 +94,12 @@ export const DayCell: React.FC<DayCellProps> = memo(
59
94
  }) => {
60
95
  const overrides = useDatePickerOverrides();
61
96
 
62
- // Build cell style based on state and overrides
63
- // Use primaryColor as fallback for cellSelectedColor (consumer expectation)
64
- const cellStyle = useMemo(() => {
65
- const style: Record<string, unknown> = {
66
- backgroundColor: DEFAULT_COLORS.cellBackground,
67
- };
68
-
69
- // Range start/end get selected styling
70
- if (isRangeStart || isRangeEnd || isSelected) {
71
- style.backgroundColor =
72
- overrides?.cellSelectedColor ??
73
- overrides?.primaryColor ??
74
- DEFAULT_COLORS.cellSelected;
75
- style.borderRadius =
76
- overrides?.cellBorderRadius ?? DEFAULT_COLORS.cellBorderRadius;
77
- } else if (isInRange) {
78
- // Dates in range get lighter background
79
- style.backgroundColor =
80
- overrides?.cellInRangeColor ?? DEFAULT_COLORS.cellInRange;
81
- } else if (isToday) {
82
- style.backgroundColor =
83
- overrides?.cellTodayColor ?? DEFAULT_COLORS.cellToday;
84
- style.borderRadius =
85
- overrides?.cellBorderRadius ?? DEFAULT_COLORS.cellBorderRadius;
86
- style.borderWidth = 1;
87
- style.borderColor = overrides?.primaryColor ?? DEFAULT_COLORS.primary;
88
- }
89
-
90
- if (isDisabled) {
91
- style.opacity = 0.4;
92
- }
93
-
94
- return style;
95
- }, [
96
- overrides,
97
- isSelected,
98
- isToday,
99
- isDisabled,
100
- isRangeStart,
101
- isRangeEnd,
102
- isInRange,
103
- ]);
97
+ const rangeColor =
98
+ overrides?.cellInRangeColor ?? DEFAULT_COLORS.cellInRange;
99
+ const selectedColor =
100
+ overrides?.cellSelectedColor ??
101
+ overrides?.primaryColor ??
102
+ DEFAULT_COLORS.cellSelected;
104
103
 
105
104
  // Build text style based on state and overrides
106
105
  const textStyle = useMemo(() => {
@@ -137,13 +136,36 @@ export const DayCell: React.FC<DayCellProps> = memo(
137
136
  isInRange,
138
137
  ]);
139
138
 
139
+ // Today style (non-range)
140
+ const todayStyle = useMemo(() => {
141
+ if (
142
+ isToday &&
143
+ !isSelected &&
144
+ !isRangeStart &&
145
+ !isRangeEnd &&
146
+ !isInRange
147
+ ) {
148
+ return {
149
+ backgroundColor:
150
+ overrides?.cellTodayColor ?? DEFAULT_COLORS.cellToday,
151
+ borderRadius:
152
+ overrides?.cellBorderRadius ?? DEFAULT_COLORS.cellBorderRadius,
153
+ borderWidth: 1,
154
+ borderColor: overrides?.primaryColor ?? DEFAULT_COLORS.primary,
155
+ };
156
+ }
157
+ return null;
158
+ }, [isToday, isSelected, isRangeStart, isRangeEnd, isInRange, overrides]);
159
+
140
160
  if (!date) {
141
161
  return <Pressable style={webStyles.cell} disabled />;
142
162
  }
143
163
 
164
+ const cellOpacity = isDisabled ? 0.4 : 1;
165
+
144
166
  return (
145
167
  <Pressable
146
- style={[webStyles.cell, cellStyle]}
168
+ style={[webStyles.cell, { opacity: cellOpacity }]}
147
169
  onPress={onPress}
148
170
  disabled={isDisabled}
149
171
  accessibilityRole="button"
@@ -151,9 +173,46 @@ export const DayCell: React.FC<DayCellProps> = memo(
151
173
  weekday: 'long',
152
174
  month: 'long',
153
175
  day: 'numeric',
176
+ timeZone: 'UTC',
154
177
  }).format(date)}
155
178
  accessibilityState={{ selected: isSelected, disabled: isDisabled }}
156
179
  >
180
+ {/* Thermometer band for range start (extends right) */}
181
+ {isRangeStart && !isRangeEnd && (
182
+ <View
183
+ style={[webStyles.rangeBandRight, { backgroundColor: rangeColor }]}
184
+ />
185
+ )}
186
+
187
+ {/* Thermometer band for range end (extends left) */}
188
+ {isRangeEnd && !isRangeStart && (
189
+ <View
190
+ style={[webStyles.rangeBandLeft, { backgroundColor: rangeColor }]}
191
+ />
192
+ )}
193
+
194
+ {/* Thermometer band for in-range dates (full width) */}
195
+ {isInRange && (
196
+ <View
197
+ style={[webStyles.rangeBand, { backgroundColor: rangeColor }]}
198
+ />
199
+ )}
200
+
201
+ {/* Circle for selected/range start/end */}
202
+ {(isRangeStart || isRangeEnd || isSelected) && (
203
+ <View
204
+ style={[
205
+ webStyles.circleOverlay,
206
+ { backgroundColor: selectedColor },
207
+ ]}
208
+ />
209
+ )}
210
+
211
+ {/* Today indicator (when not in range) */}
212
+ {todayStyle ? (
213
+ <View style={[webStyles.circleOverlay, todayStyle]} />
214
+ ) : null}
215
+
157
216
  <Text style={[webStyles.text, textStyle]}>{date.getUTCDate()}</Text>
158
217
  </Pressable>
159
218
  );
@@ -1,11 +1,27 @@
1
1
  /// <reference lib="dom" />
2
2
  import type React from 'react';
3
- import { memo, useCallback, useState } from 'react';
3
+ import { memo, useCallback, useMemo, useState } from 'react';
4
4
  import { Pressable, StyleSheet, Text, View } from 'react-native';
5
+ import { useTimePickerOverrides } from '../../context/ThemeOverrideContext';
5
6
  import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
6
7
  import type { ClockMode, TimePeriod, TimeValue } from '../../types/timepicker';
7
8
  import { ClockFace } from './ClockFace';
8
9
 
10
+ // Default colors (dark theme)
11
+ const DEFAULT_COLORS = {
12
+ containerBackground: '#2C2C2E',
13
+ headerColor: '#8E8E93',
14
+ timeFieldBackground: '#3A3A3C',
15
+ timeFieldActiveBackground: '#007AFF',
16
+ textColor: '#FFFFFF',
17
+ separatorColor: '#FFFFFF',
18
+ borderColor: '#48484A',
19
+ periodActiveBackground: 'rgba(0, 122, 255, 0.2)',
20
+ periodTextColor: '#8E8E93',
21
+ periodTextActiveColor: '#007AFF',
22
+ actionButtonColor: '#007AFF',
23
+ };
24
+
9
25
  interface MaterialTimePickerProps {
10
26
  value: TimeValue;
11
27
  onChange: (time: TimeValue) => void;
@@ -127,6 +143,7 @@ const webStyles = StyleSheet.create({
127
143
 
128
144
  export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
129
145
  ({ value, onChange, is24Hour = false, onCancel, onConfirm }) => {
146
+ const overrides = useTimePickerOverrides();
130
147
  const [mode, setMode] = useState<ClockMode>('hours');
131
148
  const { hour: hour12, period } = to12Hour(value.hours);
132
149
 
@@ -155,9 +172,95 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
155
172
  : hour12.toString().padStart(2, '0');
156
173
  const displayMinute = value.minutes.toString().padStart(2, '0');
157
174
 
175
+ // Build override styles from themeOverrides
176
+ const containerStyle = useMemo(
177
+ () => ({
178
+ backgroundColor:
179
+ overrides?.containerBackground ?? DEFAULT_COLORS.containerBackground,
180
+ }),
181
+ [overrides],
182
+ );
183
+
184
+ const headerStyle = useMemo(
185
+ () => ({
186
+ color: overrides?.headerColor ?? DEFAULT_COLORS.headerColor,
187
+ }),
188
+ [overrides],
189
+ );
190
+
191
+ const timeFieldStyle = useMemo(
192
+ () => ({
193
+ backgroundColor:
194
+ overrides?.timeFieldBackground ?? DEFAULT_COLORS.timeFieldBackground,
195
+ }),
196
+ [overrides],
197
+ );
198
+
199
+ const timeFieldActiveStyle = useMemo(
200
+ () => ({
201
+ backgroundColor:
202
+ overrides?.timeFieldActiveBackground ??
203
+ DEFAULT_COLORS.timeFieldActiveBackground,
204
+ }),
205
+ [overrides],
206
+ );
207
+
208
+ const textStyle = useMemo(
209
+ () => ({
210
+ color: overrides?.textColor ?? DEFAULT_COLORS.textColor,
211
+ }),
212
+ [overrides],
213
+ );
214
+
215
+ const separatorStyle = useMemo(
216
+ () => ({
217
+ color: overrides?.separatorColor ?? DEFAULT_COLORS.separatorColor,
218
+ }),
219
+ [overrides],
220
+ );
221
+
222
+ const periodContainerStyle = useMemo(
223
+ () => ({
224
+ borderColor: overrides?.periodBorderColor ?? DEFAULT_COLORS.borderColor,
225
+ }),
226
+ [overrides],
227
+ );
228
+
229
+ const periodActiveStyle = useMemo(
230
+ () => ({
231
+ backgroundColor:
232
+ overrides?.periodActiveBackground ??
233
+ DEFAULT_COLORS.periodActiveBackground,
234
+ }),
235
+ [overrides],
236
+ );
237
+
238
+ const periodTextStyle = useMemo(
239
+ () => ({
240
+ color: overrides?.periodTextColor ?? DEFAULT_COLORS.periodTextColor,
241
+ }),
242
+ [overrides],
243
+ );
244
+
245
+ const periodTextActiveStyle = useMemo(
246
+ () => ({
247
+ color:
248
+ overrides?.periodTextActiveColor ??
249
+ DEFAULT_COLORS.periodTextActiveColor,
250
+ }),
251
+ [overrides],
252
+ );
253
+
254
+ const actionButtonTextStyle = useMemo(
255
+ () => ({
256
+ color: overrides?.actionButtonColor ?? DEFAULT_COLORS.actionButtonColor,
257
+ }),
258
+ [overrides],
259
+ );
260
+
158
261
  return (
159
- <View style={webStyles.materialContainer}>
160
- <Text style={webStyles.materialHeader}>Select time</Text>
262
+ <View style={[webStyles.materialContainer, containerStyle]}>
263
+ <Text style={[webStyles.materialHeader, headerStyle]}>Select time</Text>
161
264
 
162
265
  <View style={webStyles.timeInputContainer}>
163
266
  <View style={webStyles.timeFieldsContainer}>
@@ -165,12 +268,17 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
165
268
  onPress={handleHourPress}
166
269
  style={[
167
270
  webStyles.timeField,
168
- mode === 'hours' && webStyles.timeFieldActive,
271
+ timeFieldStyle,
272
+ mode === 'hours' && [
273
+ webStyles.timeFieldActive,
274
+ timeFieldActiveStyle,
275
+ ],
169
276
  ]}
170
277
  >
171
278
  <Text
172
279
  style={[
173
280
  webStyles.timeFieldText,
281
+ textStyle,
174
282
  mode === 'hours' && webStyles.timeFieldTextActive,
175
283
  ]}
176
284
  >
@@ -178,18 +286,23 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
178
286
  </Text>
179
287
  </Pressable>
180
288
 
181
- <Text style={webStyles.timeSeparator}>:</Text>
289
+ <Text style={[webStyles.timeSeparator, separatorStyle]}>:</Text>
182
290
 
183
291
  <Pressable
184
292
  onPress={handleMinutePress}
185
293
  style={[
186
294
  webStyles.timeField,
187
- mode === 'minutes' && webStyles.timeFieldActive,
295
+ timeFieldStyle,
296
+ mode === 'minutes' && [
297
+ webStyles.timeFieldActive,
298
+ timeFieldActiveStyle,
299
+ ],
188
300
  ]}
189
301
  >
190
302
  <Text
191
303
  style={[
192
304
  webStyles.timeFieldText,
305
+ textStyle,
193
306
  mode === 'minutes' && webStyles.timeFieldTextActive,
194
307
  ]}
195
308
  >
@@ -199,19 +312,28 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
199
312
  </View>
200
313
 
201
314
  {!is24Hour && (
202
- <View style={webStyles.periodToggleContainer}>
315
+ <View
316
+ style={[webStyles.periodToggleContainer, periodContainerStyle]}
317
+ >
203
318
  <Pressable
204
319
  onPress={() => handlePeriodChange('AM')}
205
320
  style={[
206
321
  webStyles.periodButton,
207
322
  webStyles.periodButtonTop,
208
- period === 'AM' && webStyles.periodButtonActive,
323
+ period === 'AM' && [
324
+ webStyles.periodButtonActive,
325
+ periodActiveStyle,
326
+ ],
209
327
  ]}
210
328
  >
211
329
  <Text
212
330
  style={[
213
331
  webStyles.periodButtonText,
214
- period === 'AM' && webStyles.periodButtonTextActive,
332
+ periodTextStyle,
333
+ period === 'AM' && [
334
+ webStyles.periodButtonTextActive,
335
+ periodTextActiveStyle,
336
+ ],
215
337
  ]}
216
338
  >
217
339
  AM
@@ -221,13 +343,20 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
221
343
  onPress={() => handlePeriodChange('PM')}
222
344
  style={[
223
345
  webStyles.periodButton,
224
- period === 'PM' && webStyles.periodButtonActive,
346
+ period === 'PM' && [
347
+ webStyles.periodButtonActive,
348
+ periodActiveStyle,
349
+ ],
225
350
  ]}
226
351
  >
227
352
  <Text
228
353
  style={[
229
354
  webStyles.periodButtonText,
230
- period === 'PM' && webStyles.periodButtonTextActive,
355
+ periodTextStyle,
356
+ period === 'PM' && [
357
+ webStyles.periodButtonTextActive,
358
+ periodTextActiveStyle,
359
+ ],
231
360
  ]}
232
361
  >
233
362
  PM
@@ -253,12 +382,20 @@ export const MaterialTimePicker: React.FC<MaterialTimePickerProps> = memo(
253
382
  <View style={webStyles.actionButtonsContainer}>
254
383
  {onCancel && (
255
384
  <Pressable style={webStyles.actionButton} onPress={onCancel}>
256
- <Text style={webStyles.actionButtonText}>Cancel</Text>
385
+ <Text
386
+ style={[webStyles.actionButtonText, actionButtonTextStyle]}
387
+ >
388
+ Cancel
389
+ </Text>
257
390
  </Pressable>
258
391
  )}
259
392
  {onConfirm && (
260
393
  <Pressable style={webStyles.actionButton} onPress={onConfirm}>
261
- <Text style={webStyles.actionButtonText}>OK</Text>
394
+ <Text
395
+ style={[webStyles.actionButtonText, actionButtonTextStyle]}
396
+ >
397
+ OK
398
+ </Text>
262
399
  </Pressable>
263
400
  )}
264
401
  </View>
package/src/utils/date.ts CHANGED
@@ -157,9 +157,11 @@ export const getMonthDays = (year: number, month: number): Date[] => {
157
157
 
158
158
  /**
159
159
  * Gets the first day of the month
160
+ * Falls back to today if date is undefined/null
160
161
  */
161
- export const getFirstDayOfMonth = (date: Date): Date => {
162
- return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
162
+ export const getFirstDayOfMonth = (date: Date | null | undefined): Date => {
163
+ const d = date ?? today();
164
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
163
165
  };
164
166
 
165
167
  /**
@@ -184,7 +186,10 @@ export const formatMonth = (
184
186
  locale = 'en-US',
185
187
  style: 'long' | 'short' | 'narrow' = 'long',
186
188
  ): string => {
187
- return new Intl.DateTimeFormat(locale, { month: style }).format(date);
189
+ return new Intl.DateTimeFormat(locale, {
190
+ month: style,
191
+ timeZone: 'UTC',
192
+ }).format(date);
188
193
  };
189
194
 
190
195
  /**
@@ -195,7 +200,10 @@ export const formatWeekday = (
195
200
  locale = 'en-US',
196
201
  style: 'long' | 'short' | 'narrow' = 'short',
197
202
  ): string => {
198
- return new Intl.DateTimeFormat(locale, { weekday: style }).format(date);
203
+ return new Intl.DateTimeFormat(locale, {
204
+ weekday: style,
205
+ timeZone: 'UTC',
206
+ }).format(date);
199
207
  };
200
208
 
201
209
  /**
@@ -205,6 +213,7 @@ export const formatYearMonth = (date: Date, locale = 'en-US'): string => {
205
213
  return new Intl.DateTimeFormat(locale, {
206
214
  month: 'long',
207
215
  year: 'numeric',
216
+ timeZone: 'UTC',
208
217
  }).format(date);
209
218
  };
210
219