@ecohouse/ui 0.1.4 → 0.1.6

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.
Files changed (61) hide show
  1. package/README.md +66 -15
  2. package/dist/index.cjs +1293 -147
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +358 -154
  5. package/dist/index.d.ts +358 -154
  6. package/dist/index.js +1278 -149
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/Avatar/Avatar.tsx +19 -71
  10. package/src/components/Badge/Badge.stories.tsx +52 -0
  11. package/src/components/Badge/Badge.tsx +131 -0
  12. package/src/components/Badge/index.ts +2 -0
  13. package/src/components/Counter/Counter.stories.tsx +46 -0
  14. package/src/components/Counter/Counter.tsx +135 -0
  15. package/src/components/Counter/index.ts +2 -0
  16. package/src/components/DatePicker/DatePicker.stories.tsx +172 -0
  17. package/src/components/DatePicker/DatePicker.tsx +628 -0
  18. package/src/components/DatePicker/index.ts +7 -0
  19. package/src/components/SegmentedToggle/SegmentedToggle.stories.tsx +63 -0
  20. package/src/components/SegmentedToggle/SegmentedToggle.tsx +221 -0
  21. package/src/components/SegmentedToggle/index.ts +2 -0
  22. package/src/components/StatCard/StatCard.stories.tsx +20 -0
  23. package/src/components/StatCard/StatCard.tsx +61 -0
  24. package/src/components/StatCard/index.ts +2 -0
  25. package/src/components/index.ts +20 -0
  26. package/src/icons/CalendarOutline/CalendarOutlineIcon.tsx +25 -0
  27. package/src/icons/CalendarOutline/CalendarOutlineIcon.web.tsx +31 -0
  28. package/src/icons/CalendarOutline/calendarOutlinePath.ts +3 -0
  29. package/src/icons/CalendarOutline/index.ts +1 -0
  30. package/src/icons/ChevronLeft/ChevronLeftIcon.tsx +21 -0
  31. package/src/icons/ChevronLeft/ChevronLeftIcon.web.tsx +27 -0
  32. package/src/icons/ChevronLeft/chevronLeftPath.ts +2 -0
  33. package/src/icons/ChevronLeft/index.ts +1 -0
  34. package/src/icons/Minus/MinusIcon.tsx +20 -0
  35. package/src/icons/Minus/MinusIcon.web.tsx +19 -0
  36. package/src/icons/Minus/index.ts +1 -0
  37. package/src/icons/Minus/minusPath.ts +1 -0
  38. package/src/icons/Plus/PlusIcon.tsx +20 -0
  39. package/src/icons/Plus/PlusIcon.web.tsx +19 -0
  40. package/src/icons/Plus/index.ts +1 -0
  41. package/src/icons/Plus/plusPath.ts +1 -0
  42. package/src/icons/Sidebar/SidebarIcon.tsx +21 -0
  43. package/src/icons/Sidebar/SidebarIcon.web.tsx +27 -0
  44. package/src/icons/Sidebar/index.ts +1 -0
  45. package/src/icons/Sidebar/sidebarPath.ts +3 -0
  46. package/src/icons/UsersAlt/UsersAltIcon.tsx +14 -0
  47. package/src/icons/UsersAlt/UsersAltIcon.web.tsx +13 -0
  48. package/src/icons/UsersAlt/index.ts +1 -0
  49. package/src/icons/UsersAlt/usersAltPath.ts +2 -0
  50. package/src/icons/index.ts +6 -0
  51. package/src/icons/registry.ts +29 -4
  52. package/src/index.ts +38 -1
  53. package/src/primitives/MenuItem/MenuItem.stories.tsx +98 -0
  54. package/src/primitives/MenuItem/MenuItem.tsx +138 -0
  55. package/src/primitives/MenuItem/index.ts +2 -0
  56. package/src/primitives/index.ts +2 -0
  57. package/src/theme/colors.test.ts +6 -0
  58. package/src/theme/colors.ts +7 -0
  59. package/src/theme/index.ts +5 -0
  60. package/src/theme/typography.ts +76 -0
  61. package/src/utils/dateUtils.ts +86 -0
@@ -0,0 +1,628 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentRef,
9
+ } from "react";
10
+ import {
11
+ Platform,
12
+ Pressable,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ type StyleProp,
17
+ type ViewProps,
18
+ type ViewStyle,
19
+ } from "react-native";
20
+ import { CalendarOutlineIcon, ChevronLeftIcon } from "../../icons";
21
+ import { colors, datePickerTypography } from "../../theme";
22
+ import {
23
+ addMonths,
24
+ formatDate,
25
+ getMonthMatrix,
26
+ isBefore,
27
+ isDateDisabled,
28
+ isSameDay,
29
+ isSameMonth,
30
+ isWithinRange,
31
+ startOfDay,
32
+ } from "../../utils/dateUtils";
33
+ import { mergeRefs } from "../../utils/mergeRefs";
34
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
35
+
36
+ /** Monday-first — override via `monthNames` for other locales. */
37
+ const DEFAULT_MONTH_NAMES = [
38
+ "Հունվար",
39
+ "Փետրվար",
40
+ "Մարտ",
41
+ "Ապրիլ",
42
+ "Մայիս",
43
+ "Հունիս",
44
+ "Հուլիս",
45
+ "Օգոստոս",
46
+ "Սեպտեմբեր",
47
+ "Հոկտեմբեր",
48
+ "Նոյեմբեր",
49
+ "Դեկտեմբեր",
50
+ ];
51
+ /** Monday-first — override via `weekdayLabels` for other locales. */
52
+ const DEFAULT_WEEKDAY_LABELS = ["ԵՐ", "ԵՔ", "ՉՐ", "ՀՆ", "ՈՒՐ", "ՇԲ", "ԿՐ"];
53
+
54
+ export type DateRange = { start: Date | null; end: Date | null };
55
+
56
+ const EMPTY_RANGE: DateRange = { start: null, end: null };
57
+
58
+ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
59
+ placeholder?: string;
60
+ open?: boolean;
61
+ defaultOpen?: boolean;
62
+ onOpenChange?: (open: boolean) => void;
63
+ disabled?: boolean;
64
+ /**
65
+ * When true (default), the calendar closes after a complete selection —
66
+ * a single date, or both ends of a range. Set false to keep it open.
67
+ */
68
+ closeOnSelect?: boolean;
69
+ minDate?: Date;
70
+ maxDate?: Date;
71
+ /** First visible month when the calendar opens; defaults to the selected value's month, else today. */
72
+ defaultVisibleMonth?: Date;
73
+ /** Monday-first full month names. Defaults to Armenian. */
74
+ monthNames?: string[];
75
+ /** Weekday abbreviations, ordered starting from `weekStartsOn`. Defaults to Armenian, Monday-first. */
76
+ weekdayLabels?: string[];
77
+ /** Formats a selected date for the trigger and day accessibility labels. Defaults to "DD.MM.YYYY". */
78
+ formatValue?: (date: Date) => string;
79
+ /** First day of the week column (0 = Sunday, 1 = Monday). Defaults to Monday. */
80
+ weekStartsOn?: 0 | 1;
81
+ style?: StyleProp<ViewStyle>;
82
+ className?: string;
83
+ };
84
+
85
+ export type DatePickerSingleProps = DatePickerBaseProps & {
86
+ range?: false;
87
+ value?: Date | null;
88
+ defaultValue?: Date | null;
89
+ onValueChange?: (value: Date | null) => void;
90
+ };
91
+
92
+ export type DatePickerRangeProps = DatePickerBaseProps & {
93
+ range: true;
94
+ value?: DateRange;
95
+ defaultValue?: DateRange;
96
+ onValueChange?: (value: DateRange) => void;
97
+ };
98
+
99
+ export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
100
+
101
+ const DEFAULT_WIDTH = 380;
102
+ const TRIGGER_HEIGHT = 44;
103
+ const TRIGGER_RADIUS = 60;
104
+ const MENU_RADIUS = 20;
105
+ const ICON_SIZE = 20;
106
+ const NAV_ICON_SIZE = 20;
107
+ const DAY_CELL_HEIGHT = 44;
108
+ const DAY_CIRCLE_SIZE = 44;
109
+
110
+ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>(
111
+ function DatePicker(props, forwardedRef) {
112
+ const {
113
+ placeholder = "Select date",
114
+ open: openProp,
115
+ defaultOpen = false,
116
+ onOpenChange,
117
+ disabled = false,
118
+ closeOnSelect = true,
119
+ minDate,
120
+ maxDate,
121
+ defaultVisibleMonth,
122
+ monthNames = DEFAULT_MONTH_NAMES,
123
+ weekdayLabels = DEFAULT_WEEKDAY_LABELS,
124
+ formatValue = formatDate,
125
+ weekStartsOn = 1,
126
+ style,
127
+ className,
128
+ accessibilityLabel,
129
+ // Pulled out only to keep them off `rest` (which is spread onto the root View);
130
+ // the real values are read from `props` directly below to preserve discriminated typing.
131
+ range: _range,
132
+ value: _value,
133
+ defaultValue: _defaultValue,
134
+ onValueChange: _onValueChange,
135
+ ...rest
136
+ } = props;
137
+
138
+ const isRange = props.range === true;
139
+ const valueProp = props.value;
140
+ const rootRef = useRef<ComponentRef<typeof View>>(null);
141
+ const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
142
+ const setRootRef = useMemo(() => mergeRefs(rootRef, forwardedRef), [forwardedRef]);
143
+
144
+ const isOpenControlled = typeof openProp === "boolean";
145
+ const isValueControlled = valueProp !== undefined;
146
+
147
+ const [uncontrolledSingle, setUncontrolledSingle] = useState<Date | null>(
148
+ !isRange ? ((props as DatePickerSingleProps).defaultValue ?? null) : null,
149
+ );
150
+ const [uncontrolledRange, setUncontrolledRange] = useState<DateRange>(
151
+ isRange ? ((props as DatePickerRangeProps).defaultValue ?? EMPTY_RANGE) : EMPTY_RANGE,
152
+ );
153
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
154
+ const [hoveredDayKey, setHoveredDayKey] = useState<number | null>(null);
155
+
156
+ const rangeValue: DateRange = isRange
157
+ ? isValueControlled
158
+ ? ((valueProp as DateRange) ?? EMPTY_RANGE)
159
+ : uncontrolledRange
160
+ : EMPTY_RANGE;
161
+ const singleValue: Date | null = !isRange
162
+ ? isValueControlled
163
+ ? ((valueProp as Date | null) ?? null)
164
+ : uncontrolledSingle
165
+ : null;
166
+
167
+ const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
168
+
169
+ const [visibleMonth, setVisibleMonth] = useState(() => {
170
+ const anchor =
171
+ defaultVisibleMonth ?? (isRange ? rangeValue.start : singleValue) ?? new Date();
172
+ return new Date(anchor.getFullYear(), anchor.getMonth(), 1);
173
+ });
174
+
175
+ useApplyWebClassName(rootRef, className);
176
+
177
+ const setOpen = useCallback(
178
+ (next: boolean) => {
179
+ if (!isOpenControlled) setUncontrolledOpen(next);
180
+ onOpenChange?.(next);
181
+ if (next) {
182
+ const anchor = (isRange ? rangeValue.start : singleValue) ?? new Date();
183
+ setVisibleMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1));
184
+ } else {
185
+ setHoveredDayKey(null);
186
+ }
187
+ },
188
+ [isOpenControlled, isRange, onOpenChange, rangeValue.start, singleValue],
189
+ );
190
+
191
+ // Close on outside click (web only; native has no document-level events).
192
+ useEffect(() => {
193
+ if (Platform.OS !== "web" || !isOpen) return;
194
+
195
+ const handlePointerDown = (event: MouseEvent | TouchEvent) => {
196
+ const root = rootRef.current as unknown as { contains?: (node: Node) => boolean } | null;
197
+ const target = event.target;
198
+ if (root?.contains && target instanceof Node && !root.contains(target)) {
199
+ setOpen(false);
200
+ }
201
+ };
202
+
203
+ document.addEventListener("mousedown", handlePointerDown);
204
+ document.addEventListener("touchstart", handlePointerDown);
205
+ return () => {
206
+ document.removeEventListener("mousedown", handlePointerDown);
207
+ document.removeEventListener("touchstart", handlePointerDown);
208
+ };
209
+ }, [isOpen, setOpen]);
210
+
211
+ const focusTrigger = useCallback(() => {
212
+ const node = triggerRef.current as unknown as { focus?: () => void } | null;
213
+ node?.focus?.();
214
+ }, []);
215
+
216
+ const closeAndFocusTrigger = useCallback(() => {
217
+ setOpen(false);
218
+ focusTrigger();
219
+ }, [focusTrigger, setOpen]);
220
+
221
+ // Escape closes the calendar and returns focus to the trigger (web only).
222
+ useEffect(() => {
223
+ if (Platform.OS !== "web" || !isOpen) return;
224
+
225
+ const handleKeyDown = (event: KeyboardEvent) => {
226
+ if (event.key === "Escape") {
227
+ event.preventDefault();
228
+ closeAndFocusTrigger();
229
+ }
230
+ };
231
+
232
+ document.addEventListener("keydown", handleKeyDown);
233
+ return () => document.removeEventListener("keydown", handleKeyDown);
234
+ }, [isOpen, closeAndFocusTrigger]);
235
+
236
+ const handleTriggerPress = () => {
237
+ if (disabled) return;
238
+ setOpen(!isOpen);
239
+ };
240
+
241
+ const handleDayPress = useCallback(
242
+ (day: Date) => {
243
+ if (disabled || isDateDisabled(day, minDate, maxDate)) return;
244
+ const picked = startOfDay(day);
245
+
246
+ if (!isRange) {
247
+ if (!isValueControlled) setUncontrolledSingle(picked);
248
+ (props as DatePickerSingleProps).onValueChange?.(picked);
249
+ if (closeOnSelect) setOpen(false);
250
+ return;
251
+ }
252
+
253
+ const { start, end } = rangeValue;
254
+ const startingNewRange = !start || (start && end);
255
+ const nextRange: DateRange = startingNewRange
256
+ ? { start: picked, end: null }
257
+ : isBefore(picked, start)
258
+ ? { start: picked, end: null }
259
+ : { start, end: picked };
260
+
261
+ if (!isValueControlled) setUncontrolledRange(nextRange);
262
+ (props as DatePickerRangeProps).onValueChange?.(nextRange);
263
+ if (closeOnSelect && nextRange.start && nextRange.end) setOpen(false);
264
+ },
265
+ [
266
+ closeOnSelect,
267
+ disabled,
268
+ isRange,
269
+ isValueControlled,
270
+ maxDate,
271
+ minDate,
272
+ props,
273
+ rangeValue,
274
+ setOpen,
275
+ ],
276
+ );
277
+
278
+ const handlePrevMonth = () => setVisibleMonth((current) => addMonths(current, -1));
279
+ const handleNextMonth = () => setVisibleMonth((current) => addMonths(current, 1));
280
+
281
+ const weeks = useMemo(
282
+ () => getMonthMatrix(visibleMonth, weekStartsOn),
283
+ [visibleMonth, weekStartsOn],
284
+ );
285
+ const today = useMemo(() => startOfDay(new Date()), []);
286
+
287
+ const hasValue = isRange ? rangeValue.start != null : singleValue != null;
288
+ const triggerLabel = isRange
289
+ ? rangeValue.start && rangeValue.end
290
+ ? `${formatValue(rangeValue.start)} - ${formatValue(rangeValue.end)}`
291
+ : rangeValue.start
292
+ ? `${formatValue(rangeValue.start)} - ...`
293
+ : placeholder
294
+ : singleValue
295
+ ? formatValue(singleValue)
296
+ : placeholder;
297
+
298
+ const triggerBorderColor = isOpen ? colors.primaryMuted : colors.grey700;
299
+ const triggerTextColor = hasValue ? colors.white : colors.grey100;
300
+
301
+ const resolvedAccessibilityLabel = accessibilityLabel ?? placeholder;
302
+
303
+ return (
304
+ <View
305
+ {...rest}
306
+ ref={setRootRef}
307
+ style={[styles.root, isOpen && styles.rootOpen, disabled && styles.disabled, style]}
308
+ accessibilityState={{ disabled, expanded: isOpen }}
309
+ >
310
+ <View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
311
+ <Pressable
312
+ ref={triggerRef}
313
+ onPress={handleTriggerPress}
314
+ disabled={disabled}
315
+ accessibilityRole="button"
316
+ accessibilityLabel={resolvedAccessibilityLabel}
317
+ accessibilityState={{ disabled, expanded: isOpen }}
318
+ style={[
319
+ styles.trigger,
320
+ { borderColor: triggerBorderColor, backgroundColor: colors.grey700 },
321
+ ]}
322
+ >
323
+ <Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
324
+ {triggerLabel}
325
+ </Text>
326
+ <View style={styles.iconSlot}>
327
+ <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
328
+ </View>
329
+ </Pressable>
330
+
331
+ {isOpen ? (
332
+ <View style={styles.menu}>
333
+ <View style={styles.calendarHeader}>
334
+ <Pressable
335
+ onPress={handlePrevMonth}
336
+ hitSlop={8}
337
+ accessibilityRole="button"
338
+ accessibilityLabel="Previous month"
339
+ style={styles.navButton}
340
+ >
341
+ <ChevronLeftIcon size={NAV_ICON_SIZE} color={colors.white} />
342
+ </Pressable>
343
+
344
+ <Text style={styles.monthHeading}>
345
+ {monthNames[visibleMonth.getMonth()]} {visibleMonth.getFullYear()}
346
+ </Text>
347
+
348
+ <Pressable
349
+ onPress={handleNextMonth}
350
+ hitSlop={8}
351
+ accessibilityRole="button"
352
+ accessibilityLabel="Next month"
353
+ style={styles.navButton}
354
+ >
355
+ <View style={styles.navIconMirror}>
356
+ <ChevronLeftIcon size={NAV_ICON_SIZE} color={colors.white} />
357
+ </View>
358
+ </Pressable>
359
+ </View>
360
+
361
+ <View>
362
+ <View style={styles.weekdayRow}>
363
+ {weekdayLabels.map((weekdayLabel, index) => (
364
+ <View key={`${weekdayLabel}-${index}`} style={styles.dayCellWrap}>
365
+ <Text style={styles.weekdayText}>{weekdayLabel}</Text>
366
+ </View>
367
+ ))}
368
+ </View>
369
+
370
+ {weeks.map((week, weekIndex) => {
371
+ let rangeHighlightStyle: { left: `${number}%`; width: `${number}%` } | null =
372
+ null;
373
+
374
+ if (isRange && rangeValue.start && rangeValue.end) {
375
+ const rangeIndices = week.reduce<number[]>((acc, day, index) => {
376
+ if (isWithinRange(day, rangeValue.start as Date, rangeValue.end as Date)) {
377
+ acc.push(index);
378
+ }
379
+ return acc;
380
+ }, []);
381
+
382
+ if (rangeIndices.length > 0) {
383
+ const first = rangeIndices[0];
384
+ const last = rangeIndices[rangeIndices.length - 1];
385
+ const startsHere = isSameDay(week[first], rangeValue.start);
386
+ const endsHere = isSameDay(week[last], rangeValue.end);
387
+ // Half a cell so the bar meets the circle center, not past it.
388
+ const cellPct = 100 / 7;
389
+ const halfCellPct = cellPct / 2;
390
+ let leftPct = first * cellPct;
391
+ let widthPct = (last - first + 1) * cellPct;
392
+ if (startsHere) {
393
+ leftPct += halfCellPct;
394
+ widthPct -= halfCellPct;
395
+ }
396
+ if (endsHere) {
397
+ widthPct -= halfCellPct;
398
+ }
399
+ // Same-day range: only the circle, no connector bar.
400
+ if (widthPct > 0) {
401
+ rangeHighlightStyle = {
402
+ left: `${leftPct}%`,
403
+ width: `${widthPct}%`,
404
+ };
405
+ }
406
+ }
407
+ }
408
+
409
+ return (
410
+ <View
411
+ key={weekIndex}
412
+ style={[styles.weekRow, weekIndex > 0 && styles.weekRowSpaced]}
413
+ >
414
+ {rangeHighlightStyle ? (
415
+ <View
416
+ pointerEvents="none"
417
+ style={[styles.rangeHighlight, rangeHighlightStyle]}
418
+ />
419
+ ) : null}
420
+
421
+ {week.map((day, dayIndex) => {
422
+ const inCurrentMonth = isSameMonth(day, visibleMonth);
423
+ const isToday = isSameDay(day, today);
424
+ const isSelectedSingle = !isRange && isSameDay(day, singleValue);
425
+ const isRangeStart = isRange && isSameDay(day, rangeValue.start);
426
+ const isRangeEnd = isRange && isSameDay(day, rangeValue.end);
427
+ const isSelectedEndpoint = isSelectedSingle || isRangeStart || isRangeEnd;
428
+ const dayDisabled = disabled || isDateDisabled(day, minDate, maxDate);
429
+ const dayKey = startOfDay(day).getTime();
430
+ const isHovered =
431
+ !dayDisabled && !isSelectedEndpoint && hoveredDayKey === dayKey;
432
+
433
+ const dayTextColor = isSelectedEndpoint
434
+ ? colors.white
435
+ : dayDisabled
436
+ ? colors.grey400
437
+ : isToday
438
+ ? colors.primary
439
+ : !inCurrentMonth
440
+ ? colors.grey300
441
+ : colors.white;
442
+
443
+ return (
444
+ <Pressable
445
+ key={dayIndex}
446
+ onPress={() => handleDayPress(day)}
447
+ disabled={dayDisabled}
448
+ onHoverIn={() => setHoveredDayKey(dayKey)}
449
+ onHoverOut={() =>
450
+ setHoveredDayKey((current) => (current === dayKey ? null : current))
451
+ }
452
+ onPressIn={() => setHoveredDayKey(dayKey)}
453
+ onPressOut={() =>
454
+ setHoveredDayKey((current) => (current === dayKey ? null : current))
455
+ }
456
+ style={styles.dayCellWrap}
457
+ accessibilityRole="button"
458
+ accessibilityLabel={formatValue(day)}
459
+ accessibilityState={{
460
+ disabled: dayDisabled,
461
+ selected: isSelectedEndpoint,
462
+ }}
463
+ >
464
+ <View
465
+ style={[
466
+ styles.dayCircle,
467
+ isSelectedEndpoint && styles.dayCircleSelected,
468
+ isHovered && styles.dayCircleHovered,
469
+ ]}
470
+ >
471
+ <Text style={[styles.dayText, { color: dayTextColor }]}>
472
+ {day.getDate()}
473
+ </Text>
474
+ </View>
475
+ {isToday && !isSelectedEndpoint ? (
476
+ <View style={styles.todayDot} />
477
+ ) : null}
478
+ </Pressable>
479
+ );
480
+ })}
481
+ </View>
482
+ );
483
+ })}
484
+ </View>
485
+ </View>
486
+ ) : null}
487
+ </View>
488
+ </View>
489
+ );
490
+ },
491
+ );
492
+
493
+ const styles = StyleSheet.create({
494
+ root: {
495
+ width: DEFAULT_WIDTH,
496
+ gap: 8,
497
+ alignSelf: "flex-start",
498
+ },
499
+ rootOpen: {
500
+ zIndex: 30,
501
+ },
502
+ disabled: {
503
+ opacity: 0.6,
504
+ },
505
+ triggerWrap: {
506
+ position: "relative",
507
+ zIndex: 1,
508
+ },
509
+ triggerWrapOpen: {
510
+ zIndex: 20,
511
+ },
512
+ trigger: {
513
+ height: TRIGGER_HEIGHT,
514
+ borderRadius: TRIGGER_RADIUS,
515
+ borderWidth: 1,
516
+ paddingVertical: 10,
517
+ paddingHorizontal: 16,
518
+ flexDirection: "row",
519
+ alignItems: "center",
520
+ gap: 10,
521
+ overflow: "hidden",
522
+ },
523
+ triggerText: {
524
+ ...datePickerTypography.field,
525
+ flex: 1,
526
+ minWidth: 0,
527
+ },
528
+ iconSlot: {
529
+ width: ICON_SIZE,
530
+ height: ICON_SIZE,
531
+ alignItems: "center",
532
+ justifyContent: "center",
533
+ flexShrink: 0,
534
+ },
535
+ menu: {
536
+ position: "absolute",
537
+ top: "100%",
538
+ left: 0,
539
+ right: 0,
540
+ marginTop: 8,
541
+ zIndex: 10,
542
+ borderRadius: MENU_RADIUS,
543
+ padding: 12,
544
+ gap: 0,
545
+ backgroundColor: colors.grey700,
546
+ ...Platform.select({
547
+ web: {
548
+ boxShadow: `0 8px 24px ${colors.black}73`,
549
+ },
550
+ default: {
551
+ elevation: 8,
552
+ },
553
+ }),
554
+ },
555
+ calendarHeader: {
556
+ height: 36,
557
+ flexDirection: "row",
558
+ alignItems: "center",
559
+ justifyContent: "space-between",
560
+ },
561
+ navButton: {
562
+ width: 36,
563
+ height: 36,
564
+ alignItems: "center",
565
+ justifyContent: "center",
566
+ },
567
+ navIconMirror: {
568
+ transform: [{ scaleX: -1 }],
569
+ },
570
+ monthHeading: {
571
+ ...datePickerTypography.monthHeading,
572
+ color: colors.white,
573
+ textAlign: "center",
574
+ },
575
+ weekdayRow: {
576
+ flexDirection: "row",
577
+ },
578
+ weekdayText: {
579
+ ...datePickerTypography.weekday,
580
+ color: colors.grey300,
581
+ textAlign: "center",
582
+ textTransform: "uppercase",
583
+ },
584
+ weekRow: {
585
+ flexDirection: "row",
586
+ position: "relative",
587
+ },
588
+ weekRowSpaced: {
589
+ marginTop: 8,
590
+ },
591
+ rangeHighlight: {
592
+ position: "absolute",
593
+ top: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
594
+ bottom: (DAY_CELL_HEIGHT - DAY_CIRCLE_SIZE) / 2,
595
+ borderRadius: DAY_CIRCLE_SIZE / 2,
596
+ backgroundColor: colors.primaryHover,
597
+ },
598
+ dayCellWrap: {
599
+ flex: 1,
600
+ height: DAY_CELL_HEIGHT,
601
+ alignItems: "center",
602
+ justifyContent: "center",
603
+ },
604
+ dayCircle: {
605
+ width: DAY_CIRCLE_SIZE,
606
+ height: DAY_CIRCLE_SIZE,
607
+ borderRadius: DAY_CIRCLE_SIZE / 2,
608
+ alignItems: "center",
609
+ justifyContent: "center",
610
+ },
611
+ dayCircleSelected: {
612
+ backgroundColor: colors.primary,
613
+ },
614
+ dayCircleHovered: {
615
+ backgroundColor: colors.grey600,
616
+ },
617
+ dayText: {
618
+ ...datePickerTypography.day,
619
+ },
620
+ todayDot: {
621
+ position: "absolute",
622
+ bottom: 2,
623
+ width: 4,
624
+ height: 4,
625
+ borderRadius: 2,
626
+ backgroundColor: colors.primary,
627
+ },
628
+ });
@@ -0,0 +1,7 @@
1
+ export { DatePicker } from "./DatePicker";
2
+ export type {
3
+ DatePickerProps,
4
+ DatePickerSingleProps,
5
+ DatePickerRangeProps,
6
+ DateRange,
7
+ } from "./DatePicker";
@@ -0,0 +1,63 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { LayoutGridIcon } from "../../icons";
5
+ import { SegmentedToggle } from "./SegmentedToggle";
6
+
7
+ const optionsWithIcons = [
8
+ { value: "grid", label: "Label", icon: LayoutGridIcon },
9
+ { value: "list", label: "Label", icon: LayoutGridIcon },
10
+ ] as const;
11
+
12
+ const optionsWithoutIcons = [
13
+ { value: "first", label: "Label" },
14
+ { value: "second", label: "Label" },
15
+ ] as const;
16
+
17
+ const meta = {
18
+ title: "Components/SegmentedToggle",
19
+ component: SegmentedToggle,
20
+ args: {
21
+ options: optionsWithIcons,
22
+ defaultValue: "grid",
23
+ },
24
+ parameters: {
25
+ backgrounds: { default: "black" },
26
+ },
27
+ } satisfies Meta<typeof SegmentedToggle>;
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const WithIcons: Story = {};
33
+
34
+ export const WithoutIcons: Story = {
35
+ args: { options: optionsWithoutIcons, defaultValue: "first" },
36
+ };
37
+
38
+ export const ContentWidth: Story = {
39
+ args: { fullWidth: false },
40
+ };
41
+
42
+ export const In400PixelContainer: Story = {
43
+ render: () => (
44
+ <View style={storyStyles.container}>
45
+ <SegmentedToggle options={optionsWithIcons} defaultValue="grid" />
46
+ </View>
47
+ ),
48
+ };
49
+
50
+ function ControlledSegmentedToggle() {
51
+ const [value, setValue] = useState("grid");
52
+ return <SegmentedToggle options={optionsWithIcons} value={value} onValueChange={setValue} />;
53
+ }
54
+
55
+ export const Controlled: Story = {
56
+ render: () => <ControlledSegmentedToggle />,
57
+ };
58
+
59
+ const storyStyles = StyleSheet.create({
60
+ container: {
61
+ width: 400,
62
+ },
63
+ });