@korsolutions/ui 0.0.20 → 0.0.22
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/dist/components/index.d.mts +50 -2
- package/dist/components/index.mjs +177 -4
- package/dist/hooks/index.d.mts +32 -2
- package/dist/hooks/index.mjs +79 -2
- package/dist/{index-CGY0mO6z.d.mts → index-vgnXBa4Z.d.mts} +98 -10
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -2
- package/dist/primitives/index.d.mts +3 -2
- package/dist/primitives/index.mjs +3 -2
- package/dist/{primitives-P_8clvQr.mjs → primitives-ApsPS0vU.mjs} +335 -118
- package/dist/{toast-manager-DSo9oN8w.mjs → toast-manager-Cdhl1ep0.mjs} +1 -1
- package/dist/use-numeric-mask-B9WZG25o.d.mts +33 -0
- package/dist/use-numeric-mask-BQlz1Pus.mjs +113 -0
- package/dist/use-relative-position-BTKEyT1F.mjs +106 -0
- package/dist/use-relative-position-DBzhrBU7.d.mts +61 -0
- package/package.json +1 -1
- package/src/components/calendar/calendar.tsx +31 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/calendar/variants/default.tsx +127 -0
- package/src/components/calendar/variants/index.ts +5 -0
- package/src/components/index.ts +2 -1
- package/src/components/input/index.ts +2 -0
- package/src/components/input/numeric-input.tsx +73 -0
- package/src/hooks/index.ts +4 -1
- package/src/hooks/use-currency-mask.ts +141 -0
- package/src/hooks/use-numeric-mask.ts +202 -0
- package/src/primitives/calendar/calendar-day.tsx +64 -0
- package/src/primitives/calendar/calendar-header.tsx +21 -0
- package/src/primitives/calendar/calendar-nav-button.tsx +60 -0
- package/src/primitives/calendar/calendar-root.tsx +41 -0
- package/src/primitives/calendar/calendar-title.tsx +23 -0
- package/src/primitives/calendar/calendar-week-labels.tsx +45 -0
- package/src/primitives/calendar/calendar-weeks.tsx +47 -0
- package/src/primitives/calendar/context.ts +23 -0
- package/src/primitives/calendar/index.ts +26 -0
- package/src/primitives/calendar/types.ts +39 -0
- package/src/primitives/dropdown-menu/context.ts +1 -1
- package/src/primitives/dropdown-menu/dropdown-menu-content.tsx +1 -1
- package/src/primitives/dropdown-menu/dropdown-menu-root.tsx +1 -1
- package/src/primitives/index.ts +1 -0
- package/src/primitives/popover/context.ts +1 -1
- package/src/primitives/popover/popover-content.tsx +1 -1
- package/src/primitives/popover/popover-root.tsx +1 -1
- package/src/utils/date-utils.ts +113 -0
- /package/src/hooks/{useRelativePosition.ts → use-relative-position.ts} +0 -0
- /package/src/hooks/{useScreenSize.ts → use-screen-size.ts} +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
export type NumericMaskFormat = "currency" | "decimal" | "integer" | "percentage";
|
|
4
|
+
|
|
5
|
+
export interface UseNumericMaskOptions {
|
|
6
|
+
format?: NumericMaskFormat;
|
|
7
|
+
locale?: string;
|
|
8
|
+
currency?: string;
|
|
9
|
+
precision?: number;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
allowNegative?: boolean;
|
|
13
|
+
onChange?: (value: number | null) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseNumericMaskReturn {
|
|
17
|
+
value: string;
|
|
18
|
+
numericValue: number | null;
|
|
19
|
+
onChangeText: (text: string) => void;
|
|
20
|
+
onBlur: () => void;
|
|
21
|
+
onFocus: () => void;
|
|
22
|
+
keyboardType: "numeric" | "decimal-pad" | "number-pad";
|
|
23
|
+
setValue: (value: number | null) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useNumericMask({
|
|
27
|
+
format = "decimal",
|
|
28
|
+
locale = "en-US",
|
|
29
|
+
currency = "USD",
|
|
30
|
+
precision = 2,
|
|
31
|
+
min,
|
|
32
|
+
max,
|
|
33
|
+
allowNegative = true,
|
|
34
|
+
onChange,
|
|
35
|
+
}: UseNumericMaskOptions = {}): UseNumericMaskReturn {
|
|
36
|
+
const [numericValue, setNumericValue] = useState<number | null>(null);
|
|
37
|
+
const [displayValue, setDisplayValue] = useState("");
|
|
38
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Determine actual precision based on format
|
|
41
|
+
const effectivePrecision = format === "integer" ? 0 : precision;
|
|
42
|
+
|
|
43
|
+
const formatValue = useCallback(
|
|
44
|
+
(num: number | null): string => {
|
|
45
|
+
if (num === null || isNaN(num)) return "";
|
|
46
|
+
|
|
47
|
+
switch (format) {
|
|
48
|
+
case "currency":
|
|
49
|
+
return new Intl.NumberFormat(locale, {
|
|
50
|
+
style: "currency",
|
|
51
|
+
currency,
|
|
52
|
+
minimumFractionDigits: effectivePrecision,
|
|
53
|
+
maximumFractionDigits: effectivePrecision,
|
|
54
|
+
}).format(num);
|
|
55
|
+
|
|
56
|
+
case "percentage":
|
|
57
|
+
return new Intl.NumberFormat(locale, {
|
|
58
|
+
style: "percent",
|
|
59
|
+
minimumFractionDigits: effectivePrecision,
|
|
60
|
+
maximumFractionDigits: effectivePrecision,
|
|
61
|
+
}).format(num / 100);
|
|
62
|
+
|
|
63
|
+
case "integer":
|
|
64
|
+
return new Intl.NumberFormat(locale, {
|
|
65
|
+
minimumFractionDigits: 0,
|
|
66
|
+
maximumFractionDigits: 0,
|
|
67
|
+
}).format(num);
|
|
68
|
+
|
|
69
|
+
case "decimal":
|
|
70
|
+
default:
|
|
71
|
+
return new Intl.NumberFormat(locale, {
|
|
72
|
+
minimumFractionDigits: effectivePrecision,
|
|
73
|
+
maximumFractionDigits: effectivePrecision,
|
|
74
|
+
}).format(num);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[format, locale, currency, effectivePrecision]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const parseValue = useCallback(
|
|
81
|
+
(text: string): number | null => {
|
|
82
|
+
// Remove currency symbols, spaces, thousand separators, and percentage signs
|
|
83
|
+
let cleaned = text.replace(/[^\d.-]/g, "");
|
|
84
|
+
|
|
85
|
+
// Handle negative sign
|
|
86
|
+
if (!allowNegative) {
|
|
87
|
+
cleaned = cleaned.replace(/-/g, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parsed = parseFloat(cleaned);
|
|
91
|
+
|
|
92
|
+
if (isNaN(parsed) || cleaned === "" || cleaned === "-") return null;
|
|
93
|
+
|
|
94
|
+
// Apply min/max constraints
|
|
95
|
+
let constrained = parsed;
|
|
96
|
+
if (min !== undefined && constrained < min) constrained = min;
|
|
97
|
+
if (max !== undefined && constrained > max) constrained = max;
|
|
98
|
+
|
|
99
|
+
return constrained;
|
|
100
|
+
},
|
|
101
|
+
[min, max, allowNegative]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const handleChangeText = useCallback(
|
|
105
|
+
(text: string) => {
|
|
106
|
+
// When focused, validate input before allowing it
|
|
107
|
+
if (isFocused) {
|
|
108
|
+
// Remove formatting characters to get raw input
|
|
109
|
+
let cleaned = text.replace(/[^\d.-]/g, "");
|
|
110
|
+
|
|
111
|
+
// Validate negative sign
|
|
112
|
+
if (!allowNegative && cleaned.includes("-")) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Ensure negative sign is only at the start
|
|
117
|
+
if (allowNegative) {
|
|
118
|
+
const negativeCount = (cleaned.match(/-/g) || []).length;
|
|
119
|
+
if (negativeCount > 1 || (cleaned.includes("-") && cleaned.indexOf("-") !== 0)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check decimal precision (skip for integer format)
|
|
125
|
+
if (effectivePrecision >= 0) {
|
|
126
|
+
const decimalIndex = cleaned.indexOf(".");
|
|
127
|
+
if (decimalIndex !== -1) {
|
|
128
|
+
const decimalPart = cleaned.substring(decimalIndex + 1);
|
|
129
|
+
|
|
130
|
+
// Prevent typing more decimals than allowed precision
|
|
131
|
+
if (decimalPart.length > effectivePrecision) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Prevent multiple decimal points
|
|
137
|
+
const decimalCount = (cleaned.match(/\./g) || []).length;
|
|
138
|
+
if (decimalCount > 1) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Prevent decimal point for integer format
|
|
143
|
+
if (format === "integer" && cleaned.includes(".")) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setDisplayValue(text);
|
|
150
|
+
const value = parseValue(text);
|
|
151
|
+
setNumericValue(value);
|
|
152
|
+
onChange?.(value);
|
|
153
|
+
},
|
|
154
|
+
[parseValue, onChange, isFocused, effectivePrecision, allowNegative, format]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const handleBlur = useCallback(() => {
|
|
158
|
+
setIsFocused(false);
|
|
159
|
+
if (numericValue !== null) {
|
|
160
|
+
setDisplayValue(formatValue(numericValue));
|
|
161
|
+
} else {
|
|
162
|
+
setDisplayValue("");
|
|
163
|
+
}
|
|
164
|
+
}, [numericValue, formatValue]);
|
|
165
|
+
|
|
166
|
+
const handleFocus = useCallback(() => {
|
|
167
|
+
setIsFocused(true);
|
|
168
|
+
if (numericValue !== null) {
|
|
169
|
+
setDisplayValue(numericValue.toString());
|
|
170
|
+
}
|
|
171
|
+
}, [numericValue]);
|
|
172
|
+
|
|
173
|
+
const setValue = useCallback(
|
|
174
|
+
(value: number | null) => {
|
|
175
|
+
setNumericValue(value);
|
|
176
|
+
if (value !== null) {
|
|
177
|
+
if (isFocused) {
|
|
178
|
+
setDisplayValue(value.toString());
|
|
179
|
+
} else {
|
|
180
|
+
setDisplayValue(formatValue(value));
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
setDisplayValue("");
|
|
184
|
+
}
|
|
185
|
+
onChange?.(value);
|
|
186
|
+
},
|
|
187
|
+
[isFocused, formatValue, onChange]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Determine keyboard type based on format
|
|
191
|
+
const keyboardType = format === "integer" ? (allowNegative ? ("numeric" as const) : ("number-pad" as const)) : ("decimal-pad" as const);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
value: displayValue,
|
|
195
|
+
numericValue,
|
|
196
|
+
onChangeText: handleChangeText,
|
|
197
|
+
onBlur: handleBlur,
|
|
198
|
+
onFocus: handleFocus,
|
|
199
|
+
keyboardType,
|
|
200
|
+
setValue,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { Text, StyleProp, TextStyle, ViewStyle, Pressable } from "react-native";
|
|
3
|
+
import { formatDate, isDateSameDay, isDateToday, isDateBefore, isDateAfter, isSameMonth } from "@/utils/date-utils";
|
|
4
|
+
import { useCalendarContext } from "./context";
|
|
5
|
+
import { CalendarDayState } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface CalendarDayProps {
|
|
8
|
+
date: Date;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
|
|
11
|
+
style?: StyleProp<ViewStyle>;
|
|
12
|
+
textStyle?: StyleProp<TextStyle>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const calculateState = (
|
|
16
|
+
date: Date,
|
|
17
|
+
selected: Date | undefined,
|
|
18
|
+
isCurrentMonth: boolean,
|
|
19
|
+
isDisabled: boolean,
|
|
20
|
+
isHovered: boolean
|
|
21
|
+
): CalendarDayState => {
|
|
22
|
+
if (isDisabled) return "disabled";
|
|
23
|
+
if (selected && isDateSameDay(date, selected)) return "selected";
|
|
24
|
+
if (isDateToday(date)) return "today";
|
|
25
|
+
if (isHovered) return "hovered";
|
|
26
|
+
if (!isCurrentMonth) return "deprioritized";
|
|
27
|
+
return "default";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function CalendarDay(props: CalendarDayProps) {
|
|
31
|
+
const calendar = useCalendarContext();
|
|
32
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
33
|
+
|
|
34
|
+
const isCurrentMonth = isSameMonth(props.date, calendar.currentMonth);
|
|
35
|
+
|
|
36
|
+
const isDisabled = useMemo(() => {
|
|
37
|
+
if (calendar.minDate && isDateBefore(props.date, calendar.minDate)) return true;
|
|
38
|
+
if (calendar.maxDate && isDateAfter(props.date, calendar.maxDate)) return true;
|
|
39
|
+
return false;
|
|
40
|
+
}, [props.date, calendar.minDate, calendar.maxDate]);
|
|
41
|
+
|
|
42
|
+
const state = calculateState(props.date, calendar.value, isCurrentMonth, isDisabled, isHovered);
|
|
43
|
+
|
|
44
|
+
const handlePress = () => {
|
|
45
|
+
console.log("Day pressed:", isDisabled, calendar.onChange);
|
|
46
|
+
if (isDisabled || !calendar.onChange) return;
|
|
47
|
+
calendar.onChange(props.date);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const composedStyle = [calendar.styles?.dayButton?.default, calendar.styles?.dayButton?.[state], props.style];
|
|
51
|
+
const composedTextStyle = [calendar.styles?.dayText?.default, calendar.styles?.dayText?.[state], props.textStyle];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Pressable
|
|
55
|
+
onPress={handlePress}
|
|
56
|
+
onHoverIn={() => setIsHovered(true)}
|
|
57
|
+
onHoverOut={() => setIsHovered(false)}
|
|
58
|
+
disabled={isDisabled}
|
|
59
|
+
style={composedStyle}
|
|
60
|
+
>
|
|
61
|
+
<Text style={composedTextStyle}>{formatDate(props.date, "d")}</Text>
|
|
62
|
+
</Pressable>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, ViewProps, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { useCalendarContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface CalendarHeaderProps extends ViewProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
style?: StyleProp<ViewStyle>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CalendarHeader(props: CalendarHeaderProps) {
|
|
11
|
+
const { children, style, ...viewProps } = props;
|
|
12
|
+
const { styles } = useCalendarContext();
|
|
13
|
+
|
|
14
|
+
const headerStyle = [styles?.header, style];
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View {...viewProps} style={headerStyle}>
|
|
18
|
+
{children}
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Pressable, PressableProps, Text, StyleProp, ViewStyle, TextStyle } from "react-native";
|
|
3
|
+
import { addMonths, subMonths } from "../../utils/date-utils";
|
|
4
|
+
import { useCalendarContext } from "./context";
|
|
5
|
+
import { CalendarNavButtonState } from "./types";
|
|
6
|
+
|
|
7
|
+
export interface CalendarNavButtonProps extends PressableProps {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
direction: "prev" | "next";
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
textStyle?: StyleProp<TextStyle>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const calculateState = (isDisabled: boolean, isHovered: boolean): CalendarNavButtonState => {
|
|
15
|
+
if (isDisabled) return "disabled";
|
|
16
|
+
if (isHovered) return "hovered";
|
|
17
|
+
return "default";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function CalendarNavButton(props: CalendarNavButtonProps) {
|
|
21
|
+
const { children = props.direction === "prev" ? "‹" : "›", direction, style, textStyle, ...pressableProps } = props;
|
|
22
|
+
const { currentMonth, setCurrentMonth, minDate, maxDate, styles } = useCalendarContext();
|
|
23
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
24
|
+
|
|
25
|
+
const isDisabled = React.useMemo(() => {
|
|
26
|
+
if (direction === "prev" && minDate) {
|
|
27
|
+
const prevMonth = subMonths(currentMonth, 1);
|
|
28
|
+
return prevMonth < minDate;
|
|
29
|
+
}
|
|
30
|
+
if (direction === "next" && maxDate) {
|
|
31
|
+
const nextMonth = addMonths(currentMonth, 1);
|
|
32
|
+
return nextMonth > maxDate;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}, [direction, currentMonth, minDate, maxDate]);
|
|
36
|
+
|
|
37
|
+
const state = calculateState(isDisabled, isHovered);
|
|
38
|
+
|
|
39
|
+
const handlePress = () => {
|
|
40
|
+
if (isDisabled) return;
|
|
41
|
+
const newMonth = direction === "prev" ? subMonths(currentMonth, 1) : addMonths(currentMonth, 1);
|
|
42
|
+
setCurrentMonth(newMonth);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buttonStyle = [styles?.navButton?.default, styles?.navButton?.[state], style];
|
|
46
|
+
const textStyleCombined = [styles?.navButtonText?.default, styles?.navButtonText?.[state], textStyle];
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Pressable
|
|
50
|
+
{...pressableProps}
|
|
51
|
+
onPress={handlePress}
|
|
52
|
+
onHoverIn={() => setIsHovered(true)}
|
|
53
|
+
onHoverOut={() => setIsHovered(false)}
|
|
54
|
+
disabled={isDisabled}
|
|
55
|
+
style={buttonStyle}
|
|
56
|
+
>
|
|
57
|
+
<Text style={textStyleCombined}>{children}</Text>
|
|
58
|
+
</Pressable>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { View, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { CalendarStyles } from "./types";
|
|
4
|
+
import { CalendarContext } from "./context";
|
|
5
|
+
|
|
6
|
+
export interface CalendarRootProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
value?: Date;
|
|
9
|
+
onChange?: (date: Date | undefined) => void;
|
|
10
|
+
defaultMonth?: Date;
|
|
11
|
+
minDate?: Date;
|
|
12
|
+
maxDate?: Date;
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
styles?: CalendarStyles;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CalendarRoot(props: CalendarRootProps) {
|
|
18
|
+
const { children, value, onChange, defaultMonth = new Date(), minDate, maxDate, style, styles, ...viewProps } = props;
|
|
19
|
+
|
|
20
|
+
const [currentMonth, setCurrentMonth] = useState<Date>(defaultMonth);
|
|
21
|
+
|
|
22
|
+
const containerStyle = [styles?.root, style];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<CalendarContext.Provider
|
|
26
|
+
value={{
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
currentMonth,
|
|
30
|
+
setCurrentMonth,
|
|
31
|
+
styles,
|
|
32
|
+
minDate,
|
|
33
|
+
maxDate,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<View {...viewProps} style={containerStyle}>
|
|
37
|
+
{children}
|
|
38
|
+
</View>
|
|
39
|
+
</CalendarContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text, TextProps, StyleProp, TextStyle } from "react-native";
|
|
3
|
+
import { formatDate } from "../../utils/date-utils";
|
|
4
|
+
import { useCalendarContext } from "./context";
|
|
5
|
+
|
|
6
|
+
export interface CalendarTitleProps extends TextProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
style?: StyleProp<TextStyle>;
|
|
9
|
+
formatStr?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CalendarTitle(props: CalendarTitleProps) {
|
|
13
|
+
const { children, style, formatStr = "MMMM yyyy", ...textProps } = props;
|
|
14
|
+
const { currentMonth, styles } = useCalendarContext();
|
|
15
|
+
|
|
16
|
+
const titleStyle = [styles?.headerTitle, style];
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Text {...textProps} style={titleStyle}>
|
|
20
|
+
{children ?? formatDate(currentMonth, formatStr)}
|
|
21
|
+
</Text>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, Text, StyleProp, ViewStyle, TextStyle } from "react-native";
|
|
3
|
+
import { useCalendarContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export interface CalendarWeekLabelProps {
|
|
6
|
+
children: string;
|
|
7
|
+
style?: StyleProp<TextStyle>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function CalendarWeekLabel(props: CalendarWeekLabelProps) {
|
|
11
|
+
const { styles } = useCalendarContext();
|
|
12
|
+
|
|
13
|
+
const composedStyle = [styles?.weekLabel, props.style];
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Text numberOfLines={1} style={composedStyle}>
|
|
17
|
+
{props.children}
|
|
18
|
+
</Text>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalendarWeekLabelsProps {
|
|
23
|
+
weekDays?: string[];
|
|
24
|
+
|
|
25
|
+
style?: StyleProp<ViewStyle>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_WEEK_DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
29
|
+
|
|
30
|
+
export function CalendarWeekLabels(props: CalendarWeekLabelsProps) {
|
|
31
|
+
const { weekDays = DEFAULT_WEEK_DAYS, style } = props;
|
|
32
|
+
const { styles } = useCalendarContext();
|
|
33
|
+
|
|
34
|
+
const composedStyle = [styles?.weekLabels, style];
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={composedStyle}>
|
|
38
|
+
{weekDays.map((day, index) => (
|
|
39
|
+
<CalendarWeekLabel key={index} style={styles?.weekLabel}>
|
|
40
|
+
{day}
|
|
41
|
+
</CalendarWeekLabel>
|
|
42
|
+
))}
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, ViewProps, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import { getWeekDays, getWeeksInMonth } from "@/utils/date-utils";
|
|
4
|
+
import { useCalendarContext } from "./context";
|
|
5
|
+
import { CalendarDay } from "./calendar-day";
|
|
6
|
+
|
|
7
|
+
export interface CalendarWeekProps extends ViewProps {
|
|
8
|
+
index: number;
|
|
9
|
+
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function CalendarWeek(props: CalendarWeekProps) {
|
|
14
|
+
const { style, ...viewProps } = props;
|
|
15
|
+
const { currentMonth, styles } = useCalendarContext();
|
|
16
|
+
|
|
17
|
+
const days = getWeekDays(currentMonth.getMonth(), currentMonth.getFullYear(), props.index);
|
|
18
|
+
|
|
19
|
+
const composedStyle = [styles?.week, style];
|
|
20
|
+
return (
|
|
21
|
+
<View {...viewProps} style={composedStyle}>
|
|
22
|
+
{days.map((day, index) => {
|
|
23
|
+
return <CalendarDay key={index} date={day} />;
|
|
24
|
+
})}
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CalendarWeeksProps extends ViewProps {
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function CalendarWeeks(props: CalendarWeeksProps) {
|
|
34
|
+
const { currentMonth, styles } = useCalendarContext();
|
|
35
|
+
|
|
36
|
+
const weeks = getWeeksInMonth(currentMonth);
|
|
37
|
+
|
|
38
|
+
const composedStyle = [styles?.weeks, props.style];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={composedStyle}>
|
|
42
|
+
{Array.from({ length: weeks }).map((_, index) => {
|
|
43
|
+
return <CalendarWeek key={index} index={index} />;
|
|
44
|
+
})}
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CalendarStyles } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface CalendarContextValue {
|
|
5
|
+
value?: Date;
|
|
6
|
+
onChange?: (date: Date | undefined) => void;
|
|
7
|
+
currentMonth: Date;
|
|
8
|
+
setCurrentMonth: (month: Date) => void;
|
|
9
|
+
minDate?: Date;
|
|
10
|
+
maxDate?: Date;
|
|
11
|
+
|
|
12
|
+
styles?: CalendarStyles;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const CalendarContext = React.createContext<CalendarContextValue | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
export const useCalendarContext = () => {
|
|
18
|
+
const context = React.useContext(CalendarContext);
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error("Calendar components must be used within CalendarRoot");
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CalendarRoot } from "./calendar-root";
|
|
2
|
+
import { CalendarHeader } from "./calendar-header";
|
|
3
|
+
import { CalendarTitle } from "./calendar-title";
|
|
4
|
+
import { CalendarNavButton } from "./calendar-nav-button";
|
|
5
|
+
import { CalendarWeekLabels } from "./calendar-week-labels";
|
|
6
|
+
import { CalendarWeeks } from "./calendar-weeks";
|
|
7
|
+
import { CalendarDay } from "./calendar-day";
|
|
8
|
+
|
|
9
|
+
export const CalendarPrimitive = {
|
|
10
|
+
Root: CalendarRoot,
|
|
11
|
+
Header: CalendarHeader,
|
|
12
|
+
Title: CalendarTitle,
|
|
13
|
+
NavButton: CalendarNavButton,
|
|
14
|
+
CalendarWeekLabels: CalendarWeekLabels,
|
|
15
|
+
Weeks: CalendarWeeks,
|
|
16
|
+
Day: CalendarDay,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type { CalendarRootProps } from "./calendar-root";
|
|
20
|
+
export type { CalendarHeaderProps } from "./calendar-header";
|
|
21
|
+
export type { CalendarTitleProps } from "./calendar-title";
|
|
22
|
+
export type { CalendarNavButtonProps } from "./calendar-nav-button";
|
|
23
|
+
export type { CalendarWeekLabelsProps } from "./calendar-week-labels";
|
|
24
|
+
export type { CalendarWeeksProps } from "./calendar-weeks";
|
|
25
|
+
export type { CalendarDayProps } from "./calendar-day";
|
|
26
|
+
export type { CalendarStyles, CalendarDayState } from "./types";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ViewStyle, TextStyle } from "react-native";
|
|
2
|
+
import { CalendarWeekProps, CalendarWeeksProps } from "./calendar-weeks";
|
|
3
|
+
import { CalendarWeekLabelProps, CalendarWeekLabelsProps } from "./calendar-week-labels";
|
|
4
|
+
import { CalendarDayProps } from "./calendar-day";
|
|
5
|
+
import { CalendarRootProps } from "./calendar-root";
|
|
6
|
+
import { CalendarHeaderProps } from "./calendar-header";
|
|
7
|
+
import { CalendarTitleProps } from "./calendar-title";
|
|
8
|
+
|
|
9
|
+
export type CalendarDayState = "default" | "selected" | "today" | "disabled" | "deprioritized" | "hovered";
|
|
10
|
+
export type CalendarNavButtonState = "default" | "disabled" | "hovered";
|
|
11
|
+
|
|
12
|
+
export interface CalendarStyles {
|
|
13
|
+
root?: CalendarRootProps["style"];
|
|
14
|
+
header?: CalendarHeaderProps["style"];
|
|
15
|
+
headerTitle?: CalendarTitleProps["style"];
|
|
16
|
+
navButton?: Partial<Record<CalendarNavButtonState, ViewStyle>>;
|
|
17
|
+
navButtonText?: Partial<Record<CalendarNavButtonState, TextStyle>>;
|
|
18
|
+
weekLabels: CalendarWeekLabelsProps["style"];
|
|
19
|
+
weekLabel?: CalendarWeekLabelProps["style"];
|
|
20
|
+
weeks?: CalendarWeekProps["style"];
|
|
21
|
+
week?: CalendarWeeksProps["style"];
|
|
22
|
+
dayButton?: Partial<Record<CalendarDayState, CalendarDayProps["style"]>>;
|
|
23
|
+
dayText?: Partial<Record<CalendarDayState, CalendarDayProps["textStyle"]>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Months = [
|
|
27
|
+
january: number,
|
|
28
|
+
february: number,
|
|
29
|
+
march: number,
|
|
30
|
+
april: number,
|
|
31
|
+
may: number,
|
|
32
|
+
june: number,
|
|
33
|
+
july: number,
|
|
34
|
+
august: number,
|
|
35
|
+
september: number,
|
|
36
|
+
october: number,
|
|
37
|
+
november: number,
|
|
38
|
+
december: number
|
|
39
|
+
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, Dispatch, useContext } from "react";
|
|
2
2
|
import { LayoutRectangle } from "react-native";
|
|
3
3
|
import { DropdownMenuStyles } from "./types";
|
|
4
|
-
import { LayoutPosition } from "@/hooks/
|
|
4
|
+
import { LayoutPosition } from "@/hooks/use-relative-position";
|
|
5
5
|
|
|
6
6
|
export interface DropdownMenuContext {
|
|
7
7
|
isOpen: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
3
|
import { useDropdownMenu } from "./context";
|
|
4
|
-
import { useRelativePosition } from "@/hooks/
|
|
4
|
+
import { useRelativePosition } from "@/hooks/use-relative-position";
|
|
5
5
|
|
|
6
6
|
export interface DropdownMenuContentProps {
|
|
7
7
|
children?: React.ReactNode;
|
|
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { LayoutRectangle } from "react-native";
|
|
3
3
|
import { DropdownMenuStyles } from "./types";
|
|
4
4
|
import { DropdownMenuContext } from "./context";
|
|
5
|
-
import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/
|
|
5
|
+
import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/use-relative-position";
|
|
6
6
|
|
|
7
7
|
export interface DropdownMenuRootProps {
|
|
8
8
|
children?: React.ReactNode;
|
package/src/primitives/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, Dispatch, useContext } from "react";
|
|
2
2
|
import { PopoverStyles } from "./types";
|
|
3
3
|
import { LayoutRectangle } from "react-native";
|
|
4
|
-
import { LayoutPosition } from "@/hooks/
|
|
4
|
+
import { LayoutPosition } from "@/hooks/use-relative-position";
|
|
5
5
|
|
|
6
6
|
export interface PopoverContext {
|
|
7
7
|
isOpen: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { StyleProp, View, ViewStyle } from "react-native";
|
|
3
3
|
import { usePopover } from "./context";
|
|
4
|
-
import { useRelativePosition } from "@/hooks/
|
|
4
|
+
import { useRelativePosition } from "@/hooks/use-relative-position";
|
|
5
5
|
|
|
6
6
|
export interface PopoverContentProps {
|
|
7
7
|
children?: React.ReactNode;
|
|
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { PopoverContext } from "./context";
|
|
3
3
|
import { PopoverStyles } from "./types";
|
|
4
4
|
import { LayoutRectangle } from "react-native";
|
|
5
|
-
import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/
|
|
5
|
+
import { DEFAULT_LAYOUT, DEFAULT_POSITION, LayoutPosition } from "@/hooks/use-relative-position";
|
|
6
6
|
|
|
7
7
|
export interface PopoverRootProps {
|
|
8
8
|
children?: React.ReactNode;
|