@ecohouse/ui 0.1.4 → 0.1.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.
- package/README.md +33 -13
- package/dist/index.cjs +805 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -10
- package/dist/index.d.ts +113 -10
- package/dist/index.js +802 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.tsx +19 -71
- package/src/components/DatePicker/DatePicker.stories.tsx +172 -0
- package/src/components/DatePicker/DatePicker.tsx +628 -0
- package/src/components/DatePicker/index.ts +7 -0
- package/src/components/index.ts +8 -0
- package/src/icons/CalendarOutline/CalendarOutlineIcon.tsx +25 -0
- package/src/icons/CalendarOutline/CalendarOutlineIcon.web.tsx +31 -0
- package/src/icons/CalendarOutline/calendarOutlinePath.ts +3 -0
- package/src/icons/CalendarOutline/index.ts +1 -0
- package/src/icons/ChevronLeft/ChevronLeftIcon.tsx +21 -0
- package/src/icons/ChevronLeft/ChevronLeftIcon.web.tsx +27 -0
- package/src/icons/ChevronLeft/chevronLeftPath.ts +2 -0
- package/src/icons/ChevronLeft/index.ts +1 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/registry.ts +17 -2
- package/src/index.ts +20 -1
- package/src/primitives/MenuItem/MenuItem.stories.tsx +98 -0
- package/src/primitives/MenuItem/MenuItem.tsx +138 -0
- package/src/primitives/MenuItem/index.ts +2 -0
- package/src/primitives/index.ts +2 -0
- package/src/theme/colors.ts +1 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/typography.ts +40 -0
- package/src/utils/dateUtils.ts +86 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Plain calendar icon path (no checkmark badge) — 16×16 viewBox. */
|
|
2
|
+
export const CALENDAR_OUTLINE_PATH =
|
|
3
|
+
"M10.6667 1.33334V4.00001M5.33333 1.33334V4.00001M2 6.66668H14M5.2 2.66668H10.8C11.9201 2.66668 12.4802 2.66668 12.908 2.88466C13.2843 3.07641 13.5903 3.38237 13.782 3.7587C14 4.18652 14 4.74657 14 5.86668V11.4667C14 12.5868 14 13.1468 13.782 13.5747C13.5903 13.951 13.2843 14.2569 12.908 14.4487C12.4802 14.6667 11.9201 14.6667 10.8 14.6667H5.2C4.0799 14.6667 3.51984 14.6667 3.09202 14.4487C2.71569 14.2569 2.40973 13.951 2.21799 13.5747C2 13.1468 2 12.5868 2 11.4667V5.86668C2 4.74657 2 4.18652 2.21799 3.7587C2.40973 3.38237 2.71569 3.07641 3.09202 2.88466C3.51984 2.66668 4.0799 2.66668 5.2 2.66668Z";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CalendarOutlineIcon, CALENDAR_OUTLINE_PATH } from "./CalendarOutlineIcon";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Svg, { Path } from "react-native-svg";
|
|
2
|
+
import { CHEVRON_LEFT_PATH } from "./chevronLeftPath";
|
|
3
|
+
import { colors } from "../../theme";
|
|
4
|
+
import type { IconProps } from "../types";
|
|
5
|
+
|
|
6
|
+
export { CHEVRON_LEFT_PATH } from "./chevronLeftPath";
|
|
7
|
+
|
|
8
|
+
/** Native — react-native-svg (peer dependency). */
|
|
9
|
+
export function ChevronLeftIcon({ size = 20, color = colors.primary, strokeWidth = 2 }: IconProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
|
|
12
|
+
<Path
|
|
13
|
+
d={CHEVRON_LEFT_PATH}
|
|
14
|
+
stroke={color}
|
|
15
|
+
strokeWidth={strokeWidth}
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
/>
|
|
19
|
+
</Svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CHEVRON_LEFT_PATH } from "./chevronLeftPath";
|
|
2
|
+
import { colors } from "../../theme";
|
|
3
|
+
import type { IconProps } from "../types";
|
|
4
|
+
|
|
5
|
+
/** Web / Storybook — plain SVG (no react-native-svg). */
|
|
6
|
+
export function ChevronLeftIcon({ size = 20, color = colors.primary, strokeWidth = 2 }: IconProps) {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
width={size}
|
|
10
|
+
height={size}
|
|
11
|
+
viewBox="0 0 20 20"
|
|
12
|
+
fill="none"
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
aria-hidden
|
|
15
|
+
>
|
|
16
|
+
<path
|
|
17
|
+
d={CHEVRON_LEFT_PATH}
|
|
18
|
+
stroke={color}
|
|
19
|
+
strokeWidth={strokeWidth}
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { CHEVRON_LEFT_PATH } from "./chevronLeftPath";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChevronLeftIcon, CHEVRON_LEFT_PATH } from "./ChevronLeftIcon";
|
package/src/icons/index.ts
CHANGED
|
@@ -10,10 +10,12 @@ export { BagIcon } from "./Bag";
|
|
|
10
10
|
export { BellIcon } from "./Bell";
|
|
11
11
|
export { BuildingIcon } from "./Building";
|
|
12
12
|
export { CalendarIcon } from "./Calendar";
|
|
13
|
+
export { CalendarOutlineIcon } from "./CalendarOutline";
|
|
13
14
|
export { CameraIcon } from "./Camera";
|
|
14
15
|
export { CheckIcon } from "./Check";
|
|
15
16
|
export { CheckBadgeIcon } from "./CheckBadge";
|
|
16
17
|
export { ChevronDownIcon } from "./ChevronDown";
|
|
18
|
+
export { ChevronLeftIcon } from "./ChevronLeft";
|
|
17
19
|
export { ClockIcon } from "./Clock";
|
|
18
20
|
export { FacebookIcon } from "./Facebook";
|
|
19
21
|
export { HeartIcon } from "./Heart";
|
package/src/icons/registry.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { BagIcon } from "./Bag";
|
|
|
3
3
|
import { BellIcon } from "./Bell";
|
|
4
4
|
import { BuildingIcon } from "./Building";
|
|
5
5
|
import { CalendarIcon } from "./Calendar";
|
|
6
|
+
import { CalendarOutlineIcon } from "./CalendarOutline";
|
|
6
7
|
import { CameraIcon } from "./Camera";
|
|
7
8
|
import { CheckIcon } from "./Check";
|
|
8
9
|
import { CheckBadgeIcon } from "./CheckBadge";
|
|
9
10
|
import { ChevronDownIcon } from "./ChevronDown";
|
|
11
|
+
import { ChevronLeftIcon } from "./ChevronLeft";
|
|
10
12
|
import { ClockIcon } from "./Clock";
|
|
11
13
|
import { FacebookIcon } from "./Facebook";
|
|
12
14
|
import { HeartIcon } from "./Heart";
|
|
@@ -33,10 +35,12 @@ export const icons = {
|
|
|
33
35
|
bell: BellIcon,
|
|
34
36
|
building: BuildingIcon,
|
|
35
37
|
calendar: CalendarIcon,
|
|
38
|
+
calendarOutline: CalendarOutlineIcon,
|
|
36
39
|
camera: CameraIcon,
|
|
37
40
|
check: CheckIcon,
|
|
38
41
|
checkBadge: CheckBadgeIcon,
|
|
39
42
|
chevronDown: ChevronDownIcon,
|
|
43
|
+
chevronLeft: ChevronLeftIcon,
|
|
40
44
|
clock: ClockIcon,
|
|
41
45
|
facebook: FacebookIcon,
|
|
42
46
|
heart: HeartIcon,
|
|
@@ -62,8 +66,19 @@ export type IconName = keyof typeof icons;
|
|
|
62
66
|
export const iconNames = Object.keys(icons) as IconName[];
|
|
63
67
|
|
|
64
68
|
export const iconGroups = {
|
|
65
|
-
navigation: ["arrowRight", "chevronDown", "home", "navigate"],
|
|
66
|
-
actions: [
|
|
69
|
+
navigation: ["arrowRight", "chevronDown", "chevronLeft", "home", "navigate"],
|
|
70
|
+
actions: [
|
|
71
|
+
"show",
|
|
72
|
+
"bell",
|
|
73
|
+
"camera",
|
|
74
|
+
"key",
|
|
75
|
+
"check",
|
|
76
|
+
"checkBadge",
|
|
77
|
+
"search",
|
|
78
|
+
"calendar",
|
|
79
|
+
"calendarOutline",
|
|
80
|
+
"heart",
|
|
81
|
+
],
|
|
67
82
|
objects: ["bag", "building", "clock", "user", "video"],
|
|
68
83
|
communication: ["mail", "phone"],
|
|
69
84
|
social: ["facebook", "instagram", "linkedin"],
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
Avatar,
|
|
3
|
+
Button,
|
|
4
|
+
Checkbox,
|
|
5
|
+
DatePicker,
|
|
6
|
+
Input,
|
|
7
|
+
Select,
|
|
8
|
+
Textarea,
|
|
9
|
+
Toggle,
|
|
10
|
+
} from "./components";
|
|
2
11
|
export type {
|
|
3
12
|
AvatarProps,
|
|
4
13
|
AvatarMenuItem,
|
|
@@ -7,6 +16,10 @@ export type {
|
|
|
7
16
|
ButtonVariant,
|
|
8
17
|
CheckboxProps,
|
|
9
18
|
CheckboxVariant,
|
|
19
|
+
DatePickerProps,
|
|
20
|
+
DatePickerSingleProps,
|
|
21
|
+
DatePickerRangeProps,
|
|
22
|
+
DateRange,
|
|
10
23
|
InputProps,
|
|
11
24
|
InputSize,
|
|
12
25
|
SelectOption,
|
|
@@ -17,6 +30,9 @@ export type {
|
|
|
17
30
|
ToggleProps,
|
|
18
31
|
} from "./components";
|
|
19
32
|
|
|
33
|
+
export { MenuItem } from "./primitives";
|
|
34
|
+
export type { MenuItemProps } from "./primitives";
|
|
35
|
+
|
|
20
36
|
export {
|
|
21
37
|
Icon,
|
|
22
38
|
icons,
|
|
@@ -29,10 +45,12 @@ export {
|
|
|
29
45
|
BellIcon,
|
|
30
46
|
BuildingIcon,
|
|
31
47
|
CalendarIcon,
|
|
48
|
+
CalendarOutlineIcon,
|
|
32
49
|
CameraIcon,
|
|
33
50
|
CheckIcon,
|
|
34
51
|
CheckBadgeIcon,
|
|
35
52
|
ChevronDownIcon,
|
|
53
|
+
ChevronLeftIcon,
|
|
36
54
|
ClockIcon,
|
|
37
55
|
FacebookIcon,
|
|
38
56
|
HeartIcon,
|
|
@@ -59,6 +77,7 @@ export {
|
|
|
59
77
|
grey,
|
|
60
78
|
avatarTypography,
|
|
61
79
|
buttonTypography,
|
|
80
|
+
datePickerTypography,
|
|
62
81
|
inputTypography,
|
|
63
82
|
selectTypography,
|
|
64
83
|
textareaTypography,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
import { View, StyleSheet } from "react-native";
|
|
4
|
+
import { MenuItem } from "./MenuItem";
|
|
5
|
+
import {
|
|
6
|
+
CalendarIcon,
|
|
7
|
+
HeartIcon,
|
|
8
|
+
HelpCircleIcon,
|
|
9
|
+
LayoutGridIcon,
|
|
10
|
+
LogOutIcon,
|
|
11
|
+
SettingsIcon,
|
|
12
|
+
VideoIcon,
|
|
13
|
+
} from "../../icons";
|
|
14
|
+
import { colors } from "../../theme";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "Primitives/MenuItem",
|
|
18
|
+
component: MenuItem,
|
|
19
|
+
args: {
|
|
20
|
+
label: "Bookings",
|
|
21
|
+
icon: <CalendarIcon size={16} />,
|
|
22
|
+
onPress: fn(),
|
|
23
|
+
disabled: false,
|
|
24
|
+
},
|
|
25
|
+
argTypes: {
|
|
26
|
+
disabled: { control: "boolean" },
|
|
27
|
+
backgroundColor: { control: "color" },
|
|
28
|
+
hoverColor: { control: "color" },
|
|
29
|
+
onPress: { action: "pressed" },
|
|
30
|
+
},
|
|
31
|
+
parameters: {
|
|
32
|
+
backgrounds: { default: "black" },
|
|
33
|
+
},
|
|
34
|
+
decorators: [
|
|
35
|
+
(Story) => (
|
|
36
|
+
<View style={storyStyles.frame}>
|
|
37
|
+
<Story />
|
|
38
|
+
</View>
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
} satisfies Meta<typeof MenuItem>;
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
export const Default: Story = {};
|
|
47
|
+
|
|
48
|
+
export const WithoutIcon: Story = {
|
|
49
|
+
args: { icon: undefined },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Disabled: Story = {
|
|
53
|
+
args: { disabled: true },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Optional resting / hover colors — transparent + grey600 by default. */
|
|
57
|
+
export const CustomColors: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
label: "Log out",
|
|
60
|
+
icon: <LogOutIcon size={16} />,
|
|
61
|
+
backgroundColor: colors.redMuted,
|
|
62
|
+
hoverColor: colors.redHover,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const AllStates: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<View style={storyStyles.column}>
|
|
69
|
+
<MenuItem label="Bookings" icon={<CalendarIcon size={16} />} onPress={fn()} />
|
|
70
|
+
<MenuItem label="Digital keys" icon={<LayoutGridIcon size={16} />} onPress={fn()} />
|
|
71
|
+
<MenuItem label="Cameras" icon={<VideoIcon size={16} />} onPress={fn()} />
|
|
72
|
+
<MenuItem label="Favorites" icon={<HeartIcon size={16} />} onPress={fn()} />
|
|
73
|
+
<MenuItem label="Support" icon={<HelpCircleIcon size={16} />} onPress={fn()} />
|
|
74
|
+
<MenuItem label="Settings" icon={<SettingsIcon size={16} />} onPress={fn()} />
|
|
75
|
+
<MenuItem
|
|
76
|
+
label="Log out"
|
|
77
|
+
icon={<LogOutIcon size={16} />}
|
|
78
|
+
onPress={fn()}
|
|
79
|
+
backgroundColor={colors.redMuted}
|
|
80
|
+
hoverColor={colors.redHover}
|
|
81
|
+
/>
|
|
82
|
+
<MenuItem label="Disabled" icon={<SettingsIcon size={16} />} disabled onPress={fn()} />
|
|
83
|
+
</View>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const storyStyles = StyleSheet.create({
|
|
88
|
+
frame: {
|
|
89
|
+
width: 282,
|
|
90
|
+
padding: 8,
|
|
91
|
+
borderRadius: 16,
|
|
92
|
+
gap: 8,
|
|
93
|
+
backgroundColor: colors.grey700,
|
|
94
|
+
},
|
|
95
|
+
column: {
|
|
96
|
+
gap: 8,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, useState, type ComponentRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type GestureResponderEvent,
|
|
8
|
+
type PressableProps,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type TextStyle,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { avatarTypography, colors } from "../../theme";
|
|
14
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
15
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
16
|
+
|
|
17
|
+
export interface MenuItemProps extends Omit<
|
|
18
|
+
PressableProps,
|
|
19
|
+
"onPress" | "disabled" | "style" | "children" | "hitSlop"
|
|
20
|
+
> {
|
|
21
|
+
/** Leading icon, e.g. `<CalendarIcon size={16} />` — pass it pre-colored. */
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
label: string;
|
|
24
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Row background at rest. Defaults to transparent. */
|
|
27
|
+
backgroundColor?: string;
|
|
28
|
+
/** Row background on hover/press. Defaults to `colors.grey600`. */
|
|
29
|
+
hoverColor?: string;
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
32
|
+
className?: string;
|
|
33
|
+
hitSlop?: PressableProps["hitSlop"];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ITEM_RADIUS = 12;
|
|
37
|
+
const ICON_SIZE = 16;
|
|
38
|
+
|
|
39
|
+
export const MenuItem = forwardRef<ComponentRef<typeof Pressable>, MenuItemProps>(function MenuItem(
|
|
40
|
+
{
|
|
41
|
+
icon,
|
|
42
|
+
label,
|
|
43
|
+
onPress,
|
|
44
|
+
disabled = false,
|
|
45
|
+
backgroundColor,
|
|
46
|
+
hoverColor,
|
|
47
|
+
style,
|
|
48
|
+
labelStyle,
|
|
49
|
+
className,
|
|
50
|
+
accessibilityLabel,
|
|
51
|
+
hitSlop = 4,
|
|
52
|
+
onHoverIn,
|
|
53
|
+
onHoverOut,
|
|
54
|
+
onPressIn,
|
|
55
|
+
onPressOut,
|
|
56
|
+
...rest
|
|
57
|
+
},
|
|
58
|
+
forwardedRef,
|
|
59
|
+
) {
|
|
60
|
+
const containerRef = useRef<ComponentRef<typeof Pressable>>(null);
|
|
61
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
62
|
+
const [hovered, setHovered] = useState(false);
|
|
63
|
+
|
|
64
|
+
useApplyWebClassName(containerRef, className);
|
|
65
|
+
|
|
66
|
+
const resolvedBackground = hovered
|
|
67
|
+
? (hoverColor ?? colors.grey600)
|
|
68
|
+
: (backgroundColor ?? "transparent");
|
|
69
|
+
const resolvedAccessibilityLabel = accessibilityLabel ?? label;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Pressable
|
|
73
|
+
{...rest}
|
|
74
|
+
ref={setContainerRef}
|
|
75
|
+
onPress={onPress}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
hitSlop={hitSlop}
|
|
78
|
+
onHoverIn={(event) => {
|
|
79
|
+
setHovered(true);
|
|
80
|
+
onHoverIn?.(event);
|
|
81
|
+
}}
|
|
82
|
+
onHoverOut={(event) => {
|
|
83
|
+
setHovered(false);
|
|
84
|
+
onHoverOut?.(event);
|
|
85
|
+
}}
|
|
86
|
+
onPressIn={(event) => {
|
|
87
|
+
setHovered(true);
|
|
88
|
+
onPressIn?.(event);
|
|
89
|
+
}}
|
|
90
|
+
onPressOut={(event) => {
|
|
91
|
+
setHovered(false);
|
|
92
|
+
onPressOut?.(event);
|
|
93
|
+
}}
|
|
94
|
+
accessibilityRole="menuitem"
|
|
95
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
96
|
+
accessibilityState={{ disabled }}
|
|
97
|
+
style={[
|
|
98
|
+
styles.root,
|
|
99
|
+
{ backgroundColor: resolvedBackground },
|
|
100
|
+
disabled && styles.disabled,
|
|
101
|
+
style,
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
{icon ? <View style={styles.iconSlot}>{icon}</View> : null}
|
|
105
|
+
<Text style={[styles.label, labelStyle]} numberOfLines={1}>
|
|
106
|
+
{label}
|
|
107
|
+
</Text>
|
|
108
|
+
</Pressable>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
root: {
|
|
114
|
+
minHeight: 40,
|
|
115
|
+
borderRadius: ITEM_RADIUS,
|
|
116
|
+
paddingVertical: 12,
|
|
117
|
+
paddingHorizontal: 12,
|
|
118
|
+
flexDirection: "row",
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
gap: 8,
|
|
121
|
+
alignSelf: "stretch",
|
|
122
|
+
},
|
|
123
|
+
disabled: {
|
|
124
|
+
opacity: 0.5,
|
|
125
|
+
},
|
|
126
|
+
iconSlot: {
|
|
127
|
+
width: ICON_SIZE,
|
|
128
|
+
height: ICON_SIZE,
|
|
129
|
+
alignItems: "center",
|
|
130
|
+
justifyContent: "center",
|
|
131
|
+
flexShrink: 0,
|
|
132
|
+
},
|
|
133
|
+
label: {
|
|
134
|
+
...avatarTypography.menuItem,
|
|
135
|
+
color: colors.white,
|
|
136
|
+
flex: 1,
|
|
137
|
+
},
|
|
138
|
+
});
|
package/src/theme/colors.ts
CHANGED
package/src/theme/index.ts
CHANGED
package/src/theme/typography.ts
CHANGED
|
@@ -95,6 +95,46 @@ export const avatarTypography = {
|
|
|
95
95
|
},
|
|
96
96
|
} as const;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* DatePicker typography — trigger field text plus the calendar-panel type
|
|
100
|
+
* (month heading, weekday header, day-cell numbers). No label/hint chrome —
|
|
101
|
+
* the Figma trigger is a standalone field. Figma didn't spec the month
|
|
102
|
+
* heading / day-cell number sizes, so those reuse the existing type scale
|
|
103
|
+
* for visual consistency with the rest of the kit.
|
|
104
|
+
*/
|
|
105
|
+
export const datePickerTypography = {
|
|
106
|
+
field: {
|
|
107
|
+
fontFamily: fonts.sans,
|
|
108
|
+
fontSize: 12,
|
|
109
|
+
fontWeight: "400" as const,
|
|
110
|
+
lineHeight: 12,
|
|
111
|
+
letterSpacing: 0.36,
|
|
112
|
+
},
|
|
113
|
+
/** Calendar month/year heading — Bold, centered, White. */
|
|
114
|
+
monthHeading: {
|
|
115
|
+
fontFamily: fonts.sans,
|
|
116
|
+
fontSize: 14,
|
|
117
|
+
fontWeight: "700" as const,
|
|
118
|
+
lineHeight: 14,
|
|
119
|
+
letterSpacing: 0,
|
|
120
|
+
},
|
|
121
|
+
/** Weekday header (ԵՐ, ԵՔ, ...) — Regular, uppercase, centered, Grey/300. */
|
|
122
|
+
weekday: {
|
|
123
|
+
fontFamily: fonts.sans,
|
|
124
|
+
fontSize: 12,
|
|
125
|
+
fontWeight: "400" as const,
|
|
126
|
+
lineHeight: 12,
|
|
127
|
+
letterSpacing: 0,
|
|
128
|
+
},
|
|
129
|
+
day: {
|
|
130
|
+
fontFamily: fonts.sans,
|
|
131
|
+
fontSize: 14,
|
|
132
|
+
fontWeight: "400" as const,
|
|
133
|
+
lineHeight: 14,
|
|
134
|
+
letterSpacing: 0,
|
|
135
|
+
},
|
|
136
|
+
} as const;
|
|
137
|
+
|
|
98
138
|
/** Textarea label / field / hint typography — Medium/Regular 12–14, letter-spacing 3%. */
|
|
99
139
|
export const textareaTypography = {
|
|
100
140
|
label: {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, dependency-free date helpers for the DatePicker component. These only
|
|
3
|
+
* ever operate on local calendar days (time-of-day is ignored/zeroed) so
|
|
4
|
+
* comparisons stay simple and predictable across the whole component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function startOfDay(date: Date): Date {
|
|
8
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isSameDay(a: Date | null | undefined, b: Date | null | undefined): boolean {
|
|
12
|
+
if (a == null || b == null) return false;
|
|
13
|
+
return (
|
|
14
|
+
a.getFullYear() === b.getFullYear() &&
|
|
15
|
+
a.getMonth() === b.getMonth() &&
|
|
16
|
+
a.getDate() === b.getDate()
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isSameMonth(a: Date, b: Date): boolean {
|
|
21
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function addMonths(date: Date, amount: number): Date {
|
|
25
|
+
return new Date(date.getFullYear(), date.getMonth() + amount, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isBefore(a: Date, b: Date): boolean {
|
|
29
|
+
return startOfDay(a).getTime() < startOfDay(b).getTime();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isAfter(a: Date, b: Date): boolean {
|
|
33
|
+
return startOfDay(a).getTime() > startOfDay(b).getTime();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isWithinRange(date: Date, start: Date, end: Date): boolean {
|
|
37
|
+
const time = startOfDay(date).getTime();
|
|
38
|
+
return time >= startOfDay(start).getTime() && time <= startOfDay(end).getTime();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clampDate(date: Date, min?: Date | null, max?: Date | null): Date {
|
|
42
|
+
if (min && isBefore(date, min)) return startOfDay(min);
|
|
43
|
+
if (max && isAfter(date, max)) return startOfDay(max);
|
|
44
|
+
return startOfDay(date);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isDateDisabled(date: Date, min?: Date | null, max?: Date | null): boolean {
|
|
48
|
+
if (min && isBefore(date, min)) return true;
|
|
49
|
+
if (max && isAfter(date, max)) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Zero-padded "DD.MM.YYYY" — matches the Figma reference formatting. */
|
|
54
|
+
export function formatDate(date: Date): string {
|
|
55
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
56
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
57
|
+
return `${day}.${month}.${date.getFullYear()}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Builds a 6×7 grid of calendar days for the month containing `monthDate`,
|
|
62
|
+
* including the leading/trailing days from adjacent months needed to fill
|
|
63
|
+
* whole weeks. `weekStartsOn` follows the JS `Date#getDay()` convention
|
|
64
|
+
* (0 = Sunday, 1 = Monday, ...).
|
|
65
|
+
*/
|
|
66
|
+
export function getMonthMatrix(monthDate: Date, weekStartsOn: 0 | 1 = 1): Date[][] {
|
|
67
|
+
const year = monthDate.getFullYear();
|
|
68
|
+
const month = monthDate.getMonth();
|
|
69
|
+
const firstOfMonth = new Date(year, month, 1);
|
|
70
|
+
|
|
71
|
+
const firstWeekday = firstOfMonth.getDay();
|
|
72
|
+
const leadingDays = (firstWeekday - weekStartsOn + 7) % 7;
|
|
73
|
+
const gridStart = new Date(year, month, 1 - leadingDays);
|
|
74
|
+
|
|
75
|
+
const weeks: Date[][] = [];
|
|
76
|
+
let cursor = gridStart;
|
|
77
|
+
for (let week = 0; week < 6; week += 1) {
|
|
78
|
+
const days: Date[] = [];
|
|
79
|
+
for (let day = 0; day < 7; day += 1) {
|
|
80
|
+
days.push(cursor);
|
|
81
|
+
cursor = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1);
|
|
82
|
+
}
|
|
83
|
+
weeks.push(days);
|
|
84
|
+
}
|
|
85
|
+
return weeks;
|
|
86
|
+
}
|