@atlure/ui 0.4.0 → 0.6.0

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 (72) hide show
  1. package/README.md +39 -6
  2. package/package.json +5 -6
  3. package/src/components/alert-dialog/alert-dialog.tsx +59 -0
  4. package/src/components/alert-dialog/utils.ts +5 -0
  5. package/src/components/avatar/avatar-group.tsx +52 -0
  6. package/src/components/avatar/avatar.tsx +54 -11
  7. package/src/components/badge/urgency-badge.tsx +21 -0
  8. package/src/components/calendar/calendar.tsx +189 -0
  9. package/src/components/checkbox/checkbox.tsx +26 -9
  10. package/src/components/chip/chip.tsx +66 -0
  11. package/src/components/date-range-picker/date-range-picker.tsx +104 -0
  12. package/src/components/dialog/dialog.tsx +137 -0
  13. package/src/components/format/date-label.tsx +41 -0
  14. package/src/components/format/distance-label.tsx +13 -0
  15. package/src/components/format/duration-label.tsx +13 -0
  16. package/src/components/format/money-label.tsx +20 -0
  17. package/src/components/picker/picker.tsx +104 -0
  18. package/src/components/progress/progress.tsx +95 -0
  19. package/src/components/progress/utils.ts +10 -0
  20. package/src/components/radio-group/radio-group-context.tsx +27 -0
  21. package/src/components/radio-group/radio-group.tsx +150 -0
  22. package/src/components/screen-header/screen-header.tsx +62 -0
  23. package/src/components/segmented-control/segmented-control.tsx +67 -0
  24. package/src/components/select/select.tsx +134 -0
  25. package/src/components/settings-row/settings-row.tsx +52 -0
  26. package/src/components/sheet/sheet.tsx +139 -0
  27. package/src/components/sheet/utils.ts +55 -0
  28. package/src/components/slider/format-label.ts +3 -0
  29. package/src/components/slider/hooks/use-slider-drag.ts +47 -0
  30. package/src/components/slider/range-slider.tsx +203 -0
  31. package/src/components/slider/slider.tsx +148 -0
  32. package/src/components/slider/utils.ts +57 -0
  33. package/src/components/star-rating/star-rating.tsx +98 -0
  34. package/src/components/switch/switch.tsx +34 -6
  35. package/src/components/tabs/tabs-context.tsx +32 -0
  36. package/src/components/tabs/tabs.tsx +239 -0
  37. package/src/components/time-picker/time-picker.tsx +131 -0
  38. package/src/components/toast/toast-context.tsx +26 -0
  39. package/src/components/toast/toast-provider.tsx +70 -0
  40. package/src/components/toast/toast.tsx +41 -0
  41. package/src/components/toast/utils.ts +2 -0
  42. package/src/index.ts +54 -0
  43. package/src/lib/calendar.ts +204 -0
  44. package/src/lib/format/date.ts +46 -0
  45. package/src/lib/format/distance.ts +36 -0
  46. package/src/lib/format/duration.ts +28 -0
  47. package/src/lib/format/money.ts +20 -0
  48. package/src/lib/locale.tsx +36 -0
  49. package/src/lib/portal.tsx +54 -0
  50. package/src/lib/touch-target.ts +5 -1
  51. package/src/lib/use-sheet.ts +18 -0
  52. package/src/lib/use-toast.ts +17 -0
  53. package/src/variants/avatar-variants.ts +68 -15
  54. package/src/variants/badge-variants.ts +8 -0
  55. package/src/variants/calendar-variants.ts +26 -0
  56. package/src/variants/checkbox-variants.ts +5 -14
  57. package/src/variants/chip-variants.ts +38 -0
  58. package/src/variants/dialog-variants.ts +11 -0
  59. package/src/variants/index.ts +9 -0
  60. package/src/variants/overlay-variants.ts +1 -0
  61. package/src/variants/progress-variants.ts +18 -0
  62. package/src/variants/radio-group-variants.ts +48 -0
  63. package/src/variants/screen-header-variants.ts +33 -0
  64. package/src/variants/segmented-control-variants.ts +41 -0
  65. package/src/variants/select-variants.ts +48 -0
  66. package/src/variants/settings-row-variants.ts +18 -0
  67. package/src/variants/sheet-variants.ts +10 -0
  68. package/src/variants/slider-variants.ts +8 -0
  69. package/src/variants/star-rating-variants.ts +28 -0
  70. package/src/variants/switch-variants.ts +26 -4
  71. package/src/variants/tabs-variants.ts +46 -0
  72. package/src/variants/toast-variants.ts +31 -0
package/README.md CHANGED
@@ -42,7 +42,7 @@ export function SitterCard({ sitter, onOpen, openLabel }) {
42
42
  return (
43
43
  <Card onPress={onOpen} accessibilityLabel={openLabel}>
44
44
  <CardContent className="flex-row items-center gap-md">
45
- <Avatar name={sitter.displayName} uri={sitter.avatarUrl} />
45
+ <Avatar name={sitter.displayName} src={sitter.avatarUrl} presence="online" />
46
46
  <Text variant="title">{sitter.displayName}</Text>
47
47
  </CardContent>
48
48
  </Card>
@@ -51,9 +51,42 @@ export function SitterCard({ sitter, onOpen, openLabel }) {
51
51
  ```
52
52
 
53
53
  Components: `Text`, `Button`, `Card` (`CardHeader`, `CardContent`, `CardFooter`), `Input`, `Label`,
54
- `Badge`, `Avatar`, `Separator`, `Skeleton`, `Switch`, `Checkbox`.
54
+ `Badge`, `Avatar` (`AvatarGroup`), `Separator`, `Skeleton`, `Switch`, `Checkbox`, `Sheet`, `Select` (`SelectItem`),
55
+ `Picker`, `Dialog`, `AlertDialog`, `Toast`, `MoneyLabel`, `DistanceLabel`, `DurationLabel`, `DateLabel`, `DateRangeLabel`.
56
+
57
+ Overlays queue through a host, so the app root mounts it once, above the navigator:
58
+
59
+ ```tsx
60
+ <PortalHost>
61
+ <ToastProvider bottomInset={insets.bottom}>
62
+ <Navigator />
63
+ </ToastProvider>
64
+ </PortalHost>
65
+ ```
66
+
67
+ `Dialog`, `AlertDialog` and `Toast` throw a named error if that host is missing, rather than
68
+ rendering nothing and leaving you to guess why.
69
+
70
+ `Avatar` accepts the image as either `src` or the older `uri`; `src` wins when both are set. `uri` is
71
+ kept only so 0.3.0 consumers do not break, and goes away before 1.0.
55
72
 
56
73
  Every user-facing string is a prop. This package contains no display copy, so i18n stays in the app.
74
+ The one exception is `MoneyLabel`'s `per` suffix, which renders the English words `per hour`, `per
75
+ night` or `per walk`; a message catalogue is out of scope for v1, so this is the single string that
76
+ must be revisited when translation lands.
77
+
78
+ ## Locale and number formatting
79
+
80
+ `LocaleProvider` carries a BCP-47 locale and a measurement system (`metric` by default, `imperial`
81
+ opt-in); `useLocale()` reads it. Without a provider the defaults are `en-IE` and `metric`.
82
+
83
+ Every unit, currency symbol, separator and date order comes from `Intl`, never from a literal — so
84
+ a price renders in its own currency regardless of the reader's locale, and `1 hr 30 mins` becomes
85
+ `1 Std. 30 Min.` in `de-DE`. Money carries minor units, and the number of decimal places is read back
86
+ from `Intl` for that currency rather than assumed to be two.
87
+
88
+ This requires `Intl` to be present. Hermes needs it switched on explicitly in the app's
89
+ `app.config.ts`; that change belongs to the app, not to this package.
57
90
 
58
91
  ## Variant recipes are shared, render layers are not
59
92
 
@@ -105,10 +138,10 @@ changing how it looks.
105
138
  `pnpm test` runs Vitest with React Testing Library. `react-native` is aliased to `react-native-web`
106
139
  so components render into jsdom and can be queried by role and accessible name.
107
140
 
108
- `@atlure/tokens` is resolved to `../tokens/src` for typechecking and testing, via `paths` in
109
- `tsconfig.json` and an alias in `vitest.config.ts`. Published consumers still get `dist` through that
110
- package's `exports`; resolving to source here means this package's checks do not depend on tokens having
111
- been built first, and can never read a stale `dist`.
141
+ `@atlure/tokens` and `@atlure/types` are resolved to `../tokens/src` and `../types/src` for
142
+ typechecking and testing, via `paths` in `tsconfig.json` and aliases in `vitest.config.ts`. Published
143
+ consumers still get `dist` through those packages' `exports`; resolving to source here means this
144
+ package's checks do not depend on either having been built first, and can never read a stale `dist`.
112
145
 
113
146
  What that proves: press handling, disabled and loading behaviour, controlled toggle state, change
114
147
  propagation, accessible roles and names, and the barrel generator's output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlure/ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Atlure React Native component library: NativeWind primitives built on @atlure/tokens, shipped as untranspiled TypeScript source",
5
5
  "license": "MIT",
6
6
  "author": "David Moreira",
@@ -37,22 +37,21 @@
37
37
  "class-variance-authority": "0.7.1",
38
38
  "clsx": "2.1.1",
39
39
  "tailwind-merge": "2.6.0",
40
- "@atlure/icons": "0.4.0",
41
- "@atlure/tokens": "0.4.0"
40
+ "@atlure/icons": "0.6.0",
41
+ "@atlure/tokens": "0.6.0",
42
+ "@atlure/types": "0.6.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "nativewind": "^4.2.6",
45
46
  "react": ">=18.3.0",
46
47
  "react-native": ">=0.81.0",
48
+ "react-native-gesture-handler": ">=2.16.0",
47
49
  "react-native-reanimated": ">=3.16.0",
48
50
  "react-native-safe-area-context": ">=4.10.0",
49
51
  "react-native-svg": ">=15.0.0",
50
52
  "tailwindcss": "^3.4.19"
51
53
  },
52
54
  "peerDependenciesMeta": {
53
- "react-native-reanimated": {
54
- "optional": true
55
- },
56
55
  "react-native-safe-area-context": {
57
56
  "optional": true
58
57
  }
@@ -0,0 +1,59 @@
1
+ import { Button } from "../button/button";
2
+ import {
3
+ Dialog,
4
+ DialogDescription,
5
+ DialogFooter,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ } from "../dialog/dialog";
9
+ import { alertDialogConfirmVariant } from "./utils";
10
+
11
+ export interface AlertDialogProps {
12
+ isOpen: boolean;
13
+ title: string;
14
+ description?: string;
15
+ confirmLabel: string;
16
+ cancelLabel: string;
17
+ onConfirm: () => void;
18
+ onCancel: () => void;
19
+ isDestructive?: boolean;
20
+ isConfirmLoading?: boolean;
21
+ className?: string;
22
+ }
23
+
24
+ export function AlertDialog({
25
+ isOpen,
26
+ title,
27
+ description,
28
+ confirmLabel,
29
+ cancelLabel,
30
+ onConfirm,
31
+ onCancel,
32
+ isDestructive = false,
33
+ isConfirmLoading = false,
34
+ className,
35
+ }: AlertDialogProps) {
36
+ return (
37
+ <Dialog
38
+ isOpen={isOpen}
39
+ onClose={onCancel}
40
+ isDismissible={false}
41
+ accessibilityLabel={title}
42
+ className={className}
43
+ >
44
+ <DialogHeader>
45
+ <DialogTitle>{title}</DialogTitle>
46
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
47
+ </DialogHeader>
48
+ <DialogFooter>
49
+ <Button label={cancelLabel} variant="outline" onPress={onCancel} />
50
+ <Button
51
+ label={confirmLabel}
52
+ variant={alertDialogConfirmVariant(isDestructive)}
53
+ isLoading={isConfirmLoading}
54
+ onPress={onConfirm}
55
+ />
56
+ </DialogFooter>
57
+ </Dialog>
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ import type { ButtonVariant } from "../button/types";
2
+
3
+ export function alertDialogConfirmVariant(isDestructive: boolean): ButtonVariant {
4
+ return isDestructive ? "destructive" : "primary";
5
+ }
@@ -0,0 +1,52 @@
1
+ import { Children, type ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+
4
+ import { cn } from "../../lib/cn";
5
+ import {
6
+ avatarFallbackVariants,
7
+ avatarGroupItemVariants,
8
+ avatarGroupVariants,
9
+ avatarVariants,
10
+ type AvatarVariantProps,
11
+ } from "../../variants/avatar-variants";
12
+ import { Text } from "../text/text";
13
+
14
+ export interface AvatarGroupProps extends Omit<ViewProps, "children"> {
15
+ children: ReactNode;
16
+ max?: number;
17
+ size?: NonNullable<AvatarVariantProps["size"]>;
18
+ overflowAccessibilityLabel?: string;
19
+ }
20
+
21
+ export function AvatarGroup({
22
+ children,
23
+ max = 4,
24
+ size = "md",
25
+ overflowAccessibilityLabel,
26
+ className,
27
+ ...viewProps
28
+ }: AvatarGroupProps) {
29
+ const avatars = Children.toArray(children);
30
+ const visibleAvatars = avatars.slice(0, max);
31
+ const overflowCount = avatars.length - visibleAvatars.length;
32
+
33
+ return (
34
+ <View accessibilityRole="list" className={cn(avatarGroupVariants(), className)} {...viewProps}>
35
+ {visibleAvatars.map((avatar, index) => (
36
+ <View key={index} className={avatarGroupItemVariants({ size, isFirst: index === 0 })}>
37
+ {avatar}
38
+ </View>
39
+ ))}
40
+ {overflowCount > 0 ? (
41
+ <View
42
+ accessibilityLabel={overflowAccessibilityLabel ?? `+${overflowCount}`}
43
+ className={avatarGroupItemVariants({ size, isFirst: false })}
44
+ >
45
+ <View className={avatarVariants({ size })}>
46
+ <Text className={avatarFallbackVariants({ size })}>{`+${overflowCount}`}</Text>
47
+ </View>
48
+ </View>
49
+ ) : null}
50
+ </View>
51
+ );
52
+ }
@@ -1,42 +1,85 @@
1
+ import { useState } from "react";
1
2
  import { Image, View, type ViewProps } from "react-native";
2
3
 
3
4
  import { cn } from "../../lib/cn";
4
5
  import {
5
6
  avatarFallbackVariants,
7
+ avatarPresenceVariants,
8
+ avatarRootVariants,
6
9
  avatarVariants,
7
10
  type AvatarVariantProps,
8
11
  } from "../../variants/avatar-variants";
12
+ import { Skeleton } from "../skeleton/skeleton";
9
13
  import { Text } from "../text/text";
10
14
  import { toInitials } from "./utils";
11
15
 
16
+ export type AvatarPresence = "online" | "offline" | "none";
17
+
12
18
  export interface AvatarProps extends ViewProps {
13
19
  name: string;
20
+ src?: string | null;
14
21
  uri?: string | null;
15
22
  size?: NonNullable<AvatarVariantProps["size"]>;
23
+ shape?: NonNullable<AvatarVariantProps["shape"]>;
24
+ presence?: AvatarPresence;
25
+ presenceAccessibilityLabel?: string;
26
+ loadingAccessibilityLabel?: string;
16
27
  hasRing?: boolean;
17
28
  }
18
29
 
19
30
  export function Avatar({
20
31
  name,
32
+ src = null,
21
33
  uri = null,
22
34
  size = "md",
35
+ shape = "circle",
36
+ presence = "none",
37
+ presenceAccessibilityLabel,
38
+ loadingAccessibilityLabel,
23
39
  hasRing = false,
24
40
  className,
25
41
  ...viewProps
26
42
  }: AvatarProps) {
27
- const initials = toInitials(name);
43
+ const [failedUri, setFailedUri] = useState<string | null>(null);
44
+ const [loadedUri, setLoadedUri] = useState<string | null>(null);
45
+
46
+ const requestedUri = src ?? uri;
47
+ const imageUri = requestedUri && requestedUri !== failedUri ? requestedUri : null;
48
+ const isLoading = imageUri !== null && imageUri !== loadedUri;
28
49
 
29
50
  return (
30
- <View
31
- accessibilityRole="image"
32
- accessibilityLabel={name}
33
- className={cn(avatarVariants({ size, hasRing }), className)}
34
- {...viewProps}
35
- >
36
- {uri ? (
37
- <Image source={{ uri }} accessibilityLabel={name} className="h-full w-full" />
38
- ) : (
39
- <Text className={avatarFallbackVariants({ size })}>{initials}</Text>
51
+ <View className={avatarRootVariants()} {...viewProps}>
52
+ <View
53
+ accessibilityRole={imageUri ? undefined : "image"}
54
+ accessibilityLabel={imageUri ? undefined : name}
55
+ className={cn(avatarVariants({ size, shape, hasRing }), className)}
56
+ >
57
+ {imageUri ? (
58
+ <>
59
+ <Image
60
+ source={{ uri: imageUri }}
61
+ accessibilityLabel={name}
62
+ className="h-full w-full"
63
+ onLoadEnd={() => setLoadedUri(imageUri)}
64
+ onError={() => setFailedUri(imageUri)}
65
+ />
66
+ {isLoading ? (
67
+ <Skeleton
68
+ shape="circle"
69
+ accessibilityLabel={loadingAccessibilityLabel ?? name}
70
+ className="absolute inset-0 h-full w-full"
71
+ />
72
+ ) : null}
73
+ </>
74
+ ) : (
75
+ <Text className={avatarFallbackVariants({ size })}>{toInitials(name)}</Text>
76
+ )}
77
+ </View>
78
+ {presence === "none" ? null : (
79
+ <View
80
+ accessibilityLabel={presenceAccessibilityLabel ?? `${name} ${presence}`}
81
+ className={avatarPresenceVariants({ size, presence })}
82
+ />
40
83
  )}
41
84
  </View>
42
85
  );
@@ -0,0 +1,21 @@
1
+ import type { Urgency } from "@atlure/types";
2
+
3
+ import type { BadgeVariantProps } from "../../variants/badge-variants";
4
+ import { Badge, type BadgeProps } from "./badge";
5
+
6
+ type BadgeVariant = NonNullable<BadgeVariantProps["variant"]>;
7
+
8
+ export const urgencyBadgeVariant: Record<Urgency, BadgeVariant> = {
9
+ low: "secondary",
10
+ medium: "warning",
11
+ high: "destructive",
12
+ };
13
+
14
+ export interface UrgencyBadgeProps extends Omit<BadgeProps, "label" | "variant"> {
15
+ urgency: Urgency;
16
+ label: string;
17
+ }
18
+
19
+ export function UrgencyBadge({ urgency, ...badgeProps }: UrgencyBadgeProps) {
20
+ return <Badge variant={urgencyBadgeVariant[urgency]} {...badgeProps} />;
21
+ }
@@ -0,0 +1,189 @@
1
+ import { ChevronLeft, ChevronRight, iconSize } from "@atlure/icons";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Pressable, View } from "react-native";
4
+
5
+ import {
6
+ addMonths,
7
+ buildMonthGrid,
8
+ firstDayOfWeek,
9
+ formatFullDate,
10
+ formatMonthLabel,
11
+ formatWeekdayShort,
12
+ isDateInRange,
13
+ } from "../../lib/calendar";
14
+ import { cn } from "../../lib/cn";
15
+ import { useLocale } from "../../lib/locale";
16
+ import {
17
+ calendarContainerClassName,
18
+ calendarDayCellClassName,
19
+ calendarDayCellDisabledClassName,
20
+ calendarDayCellInRangeClassName,
21
+ calendarDayCellRangeEndClassName,
22
+ calendarDayCellRangeStartClassName,
23
+ calendarDayCellSelectedClassName,
24
+ calendarDayLabelClassName,
25
+ calendarDayLabelOutsideMonthClassName,
26
+ calendarDayLabelSelectedClassName,
27
+ calendarHeaderClassName,
28
+ calendarMarkerDotClassName,
29
+ calendarMonthLabelClassName,
30
+ calendarNavButtonClassName,
31
+ calendarWeekRowClassName,
32
+ calendarWeekdayCellClassName,
33
+ calendarWeekdayLabelClassName,
34
+ calendarWeekdayRowClassName,
35
+ } from "../../variants/calendar-variants";
36
+ import { Text } from "../text/text";
37
+
38
+ export interface CalendarHighlight {
39
+ start?: string;
40
+ end?: string;
41
+ inProgressStart?: string;
42
+ }
43
+
44
+ export interface CalendarProps {
45
+ yearMonth: string;
46
+ selected?: string;
47
+ onSelect?: (iso: string) => void;
48
+ onMonthChange?: (yearMonth: string) => void;
49
+ disabledDates?: (iso: string) => boolean;
50
+ minDate?: string;
51
+ maxDate?: string;
52
+ markers?: readonly string[];
53
+ highlight?: CalendarHighlight;
54
+ accessibilityLabel?: string;
55
+ previousMonthAccessibilityLabel?: string;
56
+ nextMonthAccessibilityLabel?: string;
57
+ className?: string;
58
+ }
59
+
60
+ export function Calendar({
61
+ yearMonth,
62
+ selected,
63
+ onSelect,
64
+ onMonthChange,
65
+ disabledDates,
66
+ minDate,
67
+ maxDate,
68
+ markers,
69
+ highlight,
70
+ accessibilityLabel,
71
+ previousMonthAccessibilityLabel = "Previous month",
72
+ nextMonthAccessibilityLabel = "Next month",
73
+ className,
74
+ }: CalendarProps) {
75
+ const { locale } = useLocale();
76
+ const [displayYearMonth, setDisplayYearMonth] = useState(yearMonth);
77
+
78
+ useEffect(() => setDisplayYearMonth(yearMonth), [yearMonth]);
79
+
80
+ const firstDow = useMemo(() => firstDayOfWeek(locale), [locale]);
81
+ const weeks = useMemo(
82
+ () => buildMonthGrid(displayYearMonth, firstDow),
83
+ [displayYearMonth, firstDow],
84
+ );
85
+ const weekdayLabels = useMemo(
86
+ () => Array.from({ length: 7 }, (_, i) => formatWeekdayShort((firstDow + i) % 7, locale)),
87
+ [firstDow, locale],
88
+ );
89
+ const markerSet = useMemo(() => new Set(markers ?? []), [markers]);
90
+
91
+ const changeMonth = (delta: number) => {
92
+ const next = addMonths(displayYearMonth, delta);
93
+ setDisplayYearMonth(next);
94
+ onMonthChange?.(next);
95
+ };
96
+
97
+ return (
98
+ <View
99
+ accessibilityLabel={accessibilityLabel}
100
+ className={cn(calendarContainerClassName, className)}
101
+ >
102
+ <View className={calendarHeaderClassName}>
103
+ <Pressable
104
+ accessibilityRole="button"
105
+ accessibilityLabel={previousMonthAccessibilityLabel}
106
+ className={calendarNavButtonClassName}
107
+ onPress={() => changeMonth(-1)}
108
+ >
109
+ <ChevronLeft size={iconSize.md} />
110
+ </Pressable>
111
+ <Text className={calendarMonthLabelClassName}>
112
+ {formatMonthLabel(displayYearMonth, locale)}
113
+ </Text>
114
+ <Pressable
115
+ accessibilityRole="button"
116
+ accessibilityLabel={nextMonthAccessibilityLabel}
117
+ className={calendarNavButtonClassName}
118
+ onPress={() => changeMonth(1)}
119
+ >
120
+ <ChevronRight size={iconSize.md} />
121
+ </Pressable>
122
+ </View>
123
+
124
+ <View className={calendarWeekdayRowClassName}>
125
+ {weekdayLabels.map((label, weekdayIndex) => (
126
+ <View key={weekdayIndex} className={calendarWeekdayCellClassName}>
127
+ <Text className={calendarWeekdayLabelClassName}>{label}</Text>
128
+ </View>
129
+ ))}
130
+ </View>
131
+
132
+ {weeks.map((week, weekIndex) => (
133
+ <View key={weekIndex} className={calendarWeekRowClassName}>
134
+ {week.map((cell) => {
135
+ const isDisabled =
136
+ disabledDates?.(cell.iso) === true || !isDateInRange(cell.iso, minDate, maxDate);
137
+ const isSelected = selected === cell.iso;
138
+ const isRangeStart = highlight?.start === cell.iso;
139
+ const isRangeEnd = highlight?.end === cell.iso;
140
+ const isInProgressStart = highlight?.inProgressStart === cell.iso;
141
+ const isInRange =
142
+ highlight?.start !== undefined &&
143
+ highlight.end !== undefined &&
144
+ cell.iso > highlight.start &&
145
+ cell.iso < highlight.end;
146
+ const isHighlighted =
147
+ isSelected || isRangeStart || isRangeEnd || isInProgressStart;
148
+ const hasMarker = markerSet.has(cell.iso);
149
+
150
+ return (
151
+ <Pressable
152
+ key={cell.iso}
153
+ accessibilityRole="button"
154
+ accessibilityLabel={formatFullDate(cell.iso, locale)}
155
+ accessibilityState={{ disabled: isDisabled, selected: isHighlighted }}
156
+ aria-disabled={isDisabled}
157
+ aria-selected={isHighlighted}
158
+ disabled={isDisabled}
159
+ onPress={() => {
160
+ if (isDisabled) return;
161
+ onSelect?.(cell.iso);
162
+ }}
163
+ className={cn(
164
+ calendarDayCellClassName,
165
+ isInRange && calendarDayCellInRangeClassName,
166
+ isRangeStart && calendarDayCellRangeStartClassName,
167
+ isRangeEnd && calendarDayCellRangeEndClassName,
168
+ (isSelected || isInProgressStart) && calendarDayCellSelectedClassName,
169
+ isDisabled && calendarDayCellDisabledClassName,
170
+ )}
171
+ >
172
+ <Text
173
+ className={cn(
174
+ calendarDayLabelClassName,
175
+ !cell.isCurrentMonth && calendarDayLabelOutsideMonthClassName,
176
+ isHighlighted && calendarDayLabelSelectedClassName,
177
+ )}
178
+ >
179
+ {cell.dayNumber}
180
+ </Text>
181
+ {hasMarker && <View className={calendarMarkerDotClassName} />}
182
+ </Pressable>
183
+ );
184
+ })}
185
+ </View>
186
+ ))}
187
+ </View>
188
+ );
189
+ }
@@ -1,10 +1,12 @@
1
+ import { Check, iconSize, Minus } from "@atlure/icons";
1
2
  import { Pressable, type PressableProps, View } from "react-native";
2
3
 
3
4
  import { cn } from "../../lib/cn";
4
- import { touchTargetHitSlop } from "../../lib/touch-target";
5
+ import { touchTargetHitSlopForSize } from "../../lib/touch-target";
5
6
  import {
7
+ checkboxBoxSize,
6
8
  checkboxBoxVariants,
7
- checkboxIndicatorVariants,
9
+ checkboxIndicatorClassName,
8
10
  checkboxRowVariants,
9
11
  } from "../../variants/checkbox-variants";
10
12
  import { Label } from "../label/label";
@@ -14,6 +16,7 @@ export interface CheckboxProps
14
16
  isChecked: boolean;
15
17
  onValueChange: (isChecked: boolean) => void;
16
18
  label?: string;
19
+ isIndeterminate?: boolean;
17
20
  isDisabled?: boolean;
18
21
  isInvalid?: boolean;
19
22
  }
@@ -22,29 +25,43 @@ export function Checkbox({
22
25
  isChecked,
23
26
  onValueChange,
24
27
  label,
28
+ isIndeterminate = false,
25
29
  isDisabled = false,
26
30
  isInvalid = false,
27
31
  accessibilityLabel,
28
32
  className,
29
33
  ...pressableProps
30
34
  }: CheckboxProps) {
35
+ const checkedState = isIndeterminate ? "mixed" : isChecked;
36
+
31
37
  return (
32
38
  <Pressable
33
39
  accessibilityRole="checkbox"
34
40
  accessibilityLabel={accessibilityLabel ?? label}
35
- accessibilityState={{ checked: isChecked, disabled: isDisabled }}
36
- aria-checked={isChecked}
41
+ accessibilityState={{ checked: checkedState, disabled: isDisabled }}
42
+ aria-checked={checkedState}
37
43
  aria-disabled={isDisabled}
38
44
  disabled={isDisabled}
39
- hitSlop={touchTargetHitSlop("md")}
40
- onPress={() => onValueChange(!isChecked)}
45
+ hitSlop={touchTargetHitSlopForSize(checkboxBoxSize)}
46
+ onPress={() => onValueChange(isIndeterminate ? true : !isChecked)}
41
47
  className={cn(checkboxRowVariants({ isDisabled }), className)}
42
48
  {...pressableProps}
43
49
  >
44
- <View className={checkboxBoxVariants({ isChecked, isInvalid })}>
45
- <View className={checkboxIndicatorVariants({ isChecked })} />
50
+ <View
51
+ className={checkboxBoxVariants({ isSelected: isIndeterminate || isChecked, isInvalid })}
52
+ >
53
+ {isIndeterminate ? (
54
+ <Minus size={iconSize.sm} className={checkboxIndicatorClassName} />
55
+ ) : null}
56
+ {!isIndeterminate && isChecked ? (
57
+ <Check size={iconSize.sm} className={checkboxIndicatorClassName} />
58
+ ) : null}
46
59
  </View>
47
- {label ? <Label isDisabled={isDisabled} isInvalid={isInvalid}>{label}</Label> : null}
60
+ {label ? (
61
+ <Label isDisabled={isDisabled} isInvalid={isInvalid}>
62
+ {label}
63
+ </Label>
64
+ ) : null}
48
65
  </Pressable>
49
66
  );
50
67
  }
@@ -0,0 +1,66 @@
1
+ import { iconSize, X } from "@atlure/icons";
2
+ import { Pressable, type PressableProps, View, type ViewProps } from "react-native";
3
+
4
+ import { cn } from "../../lib/cn";
5
+ import { touchTargetHitSlopForSize } from "../../lib/touch-target";
6
+ import {
7
+ chipBodyClassName,
8
+ chipDismissClassName,
9
+ chipHeight,
10
+ chipLabelVariants,
11
+ chipVariants,
12
+ } from "../../variants/chip-variants";
13
+ import { Text } from "../text/text";
14
+
15
+ export interface ChipProps
16
+ extends Omit<PressableProps, "children" | "disabled" | "accessibilityState" | "style"> {
17
+ label: string;
18
+ isSelected?: boolean;
19
+ isDisabled?: boolean;
20
+ onDismiss?: () => void;
21
+ dismissAccessibilityLabel?: string;
22
+ containerClassName?: ViewProps["className"];
23
+ }
24
+
25
+ export function Chip({
26
+ label,
27
+ isSelected = false,
28
+ isDisabled = false,
29
+ onDismiss,
30
+ dismissAccessibilityLabel,
31
+ accessibilityLabel,
32
+ className,
33
+ containerClassName,
34
+ ...pressableProps
35
+ }: ChipProps) {
36
+ return (
37
+ <View className={cn(chipVariants({ isSelected, isDisabled }), containerClassName)}>
38
+ <Pressable
39
+ accessibilityRole="button"
40
+ accessibilityLabel={accessibilityLabel ?? label}
41
+ accessibilityState={{ selected: isSelected, disabled: isDisabled }}
42
+ aria-selected={isSelected}
43
+ aria-disabled={isDisabled}
44
+ disabled={isDisabled}
45
+ hitSlop={touchTargetHitSlopForSize(chipHeight)}
46
+ className={cn(chipBodyClassName, className)}
47
+ {...pressableProps}
48
+ >
49
+ <Text className={chipLabelVariants({ isSelected })}>{label}</Text>
50
+ </Pressable>
51
+ {onDismiss ? (
52
+ <Pressable
53
+ accessibilityRole="button"
54
+ accessibilityLabel={dismissAccessibilityLabel ?? `Remove ${label}`}
55
+ aria-disabled={isDisabled}
56
+ disabled={isDisabled}
57
+ hitSlop={touchTargetHitSlopForSize(chipHeight)}
58
+ onPress={onDismiss}
59
+ className={chipDismissClassName}
60
+ >
61
+ <X className={chipLabelVariants({ isSelected })} size={iconSize.sm} />
62
+ </Pressable>
63
+ ) : null}
64
+ </View>
65
+ );
66
+ }