@ecohouse/ui 0.1.3 → 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.
Files changed (63) hide show
  1. package/README.md +33 -13
  2. package/dist/index.cjs +1182 -104
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +206 -5
  5. package/dist/index.d.ts +206 -5
  6. package/dist/index.js +1171 -107
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/Avatar/Avatar.stories.tsx +133 -0
  10. package/src/components/Avatar/Avatar.tsx +337 -0
  11. package/src/components/Avatar/index.ts +2 -0
  12. package/src/components/DatePicker/DatePicker.stories.tsx +172 -0
  13. package/src/components/DatePicker/DatePicker.tsx +628 -0
  14. package/src/components/DatePicker/index.ts +7 -0
  15. package/src/components/Select/Select.tsx +1 -1
  16. package/src/components/index.ts +11 -0
  17. package/src/icons/Calendar/CalendarIcon.tsx +21 -0
  18. package/src/icons/Calendar/CalendarIcon.web.tsx +27 -0
  19. package/src/icons/Calendar/calendarPath.ts +3 -0
  20. package/src/icons/Calendar/index.ts +1 -0
  21. package/src/icons/CalendarOutline/CalendarOutlineIcon.tsx +25 -0
  22. package/src/icons/CalendarOutline/CalendarOutlineIcon.web.tsx +31 -0
  23. package/src/icons/CalendarOutline/calendarOutlinePath.ts +3 -0
  24. package/src/icons/CalendarOutline/index.ts +1 -0
  25. package/src/icons/ChevronLeft/ChevronLeftIcon.tsx +21 -0
  26. package/src/icons/ChevronLeft/ChevronLeftIcon.web.tsx +27 -0
  27. package/src/icons/ChevronLeft/chevronLeftPath.ts +2 -0
  28. package/src/icons/ChevronLeft/index.ts +1 -0
  29. package/src/icons/Heart/HeartIcon.tsx +23 -0
  30. package/src/icons/Heart/HeartIcon.web.tsx +29 -0
  31. package/src/icons/Heart/heartPath.ts +3 -0
  32. package/src/icons/Heart/index.ts +1 -0
  33. package/src/icons/HelpCircle/HelpCircleIcon.tsx +25 -0
  34. package/src/icons/HelpCircle/HelpCircleIcon.web.tsx +31 -0
  35. package/src/icons/HelpCircle/helpCirclePath.ts +3 -0
  36. package/src/icons/HelpCircle/index.ts +1 -0
  37. package/src/icons/LayoutGrid/LayoutGridIcon.tsx +25 -0
  38. package/src/icons/LayoutGrid/LayoutGridIcon.web.tsx +31 -0
  39. package/src/icons/LayoutGrid/index.ts +1 -0
  40. package/src/icons/LayoutGrid/layoutGridPath.ts +3 -0
  41. package/src/icons/LogOut/LogOutIcon.tsx +21 -0
  42. package/src/icons/LogOut/LogOutIcon.web.tsx +27 -0
  43. package/src/icons/LogOut/index.ts +1 -0
  44. package/src/icons/LogOut/logOutPath.ts +3 -0
  45. package/src/icons/Settings/SettingsIcon.tsx +21 -0
  46. package/src/icons/Settings/SettingsIcon.web.tsx +27 -0
  47. package/src/icons/Settings/index.ts +1 -0
  48. package/src/icons/Settings/settingsPath.ts +3 -0
  49. package/src/icons/Video/VideoIcon.tsx +24 -0
  50. package/src/icons/Video/VideoIcon.web.tsx +30 -0
  51. package/src/icons/Video/index.ts +1 -0
  52. package/src/icons/Video/videoPath.ts +5 -0
  53. package/src/icons/index.ts +9 -0
  54. package/src/icons/registry.ts +33 -3
  55. package/src/index.ts +30 -1
  56. package/src/primitives/MenuItem/MenuItem.stories.tsx +98 -0
  57. package/src/primitives/MenuItem/MenuItem.tsx +138 -0
  58. package/src/primitives/MenuItem/index.ts +2 -0
  59. package/src/primitives/index.ts +2 -0
  60. package/src/theme/colors.ts +2 -0
  61. package/src/theme/index.ts +2 -0
  62. package/src/theme/typography.ts +65 -0
  63. package/src/utils/dateUtils.ts +86 -0
@@ -2,22 +2,31 @@ import { ArrowRightIcon } from "./ArrowRight";
2
2
  import { BagIcon } from "./Bag";
3
3
  import { BellIcon } from "./Bell";
4
4
  import { BuildingIcon } from "./Building";
5
+ import { CalendarIcon } from "./Calendar";
6
+ import { CalendarOutlineIcon } from "./CalendarOutline";
5
7
  import { CameraIcon } from "./Camera";
6
8
  import { CheckIcon } from "./Check";
7
9
  import { CheckBadgeIcon } from "./CheckBadge";
8
10
  import { ChevronDownIcon } from "./ChevronDown";
11
+ import { ChevronLeftIcon } from "./ChevronLeft";
9
12
  import { ClockIcon } from "./Clock";
10
13
  import { FacebookIcon } from "./Facebook";
14
+ import { HeartIcon } from "./Heart";
15
+ import { HelpCircleIcon } from "./HelpCircle";
11
16
  import { HomeIcon } from "./Home";
12
17
  import { InstagramIcon } from "./Instagram";
13
18
  import { KeyIcon } from "./Key";
19
+ import { LayoutGridIcon } from "./LayoutGrid";
14
20
  import { LinkedInIcon } from "./LinkedIn";
21
+ import { LogOutIcon } from "./LogOut";
15
22
  import { MailIcon } from "./Mail";
16
23
  import { NavigateIcon } from "./Navigate";
17
24
  import { PhoneIcon } from "./Phone";
18
25
  import { SearchIcon } from "./Search";
26
+ import { SettingsIcon } from "./Settings";
19
27
  import { ShowIcon } from "./Show";
20
28
  import { UserIcon } from "./User";
29
+ import { VideoIcon } from "./Video";
21
30
  import type { IconComponent } from "./types";
22
31
 
23
32
  export const icons = {
@@ -25,22 +34,31 @@ export const icons = {
25
34
  bag: BagIcon,
26
35
  bell: BellIcon,
27
36
  building: BuildingIcon,
37
+ calendar: CalendarIcon,
38
+ calendarOutline: CalendarOutlineIcon,
28
39
  camera: CameraIcon,
29
40
  check: CheckIcon,
30
41
  checkBadge: CheckBadgeIcon,
31
42
  chevronDown: ChevronDownIcon,
43
+ chevronLeft: ChevronLeftIcon,
32
44
  clock: ClockIcon,
33
45
  facebook: FacebookIcon,
46
+ heart: HeartIcon,
47
+ helpCircle: HelpCircleIcon,
34
48
  home: HomeIcon,
35
49
  instagram: InstagramIcon,
36
50
  key: KeyIcon,
51
+ layoutGrid: LayoutGridIcon,
37
52
  linkedin: LinkedInIcon,
53
+ logOut: LogOutIcon,
38
54
  mail: MailIcon,
39
55
  navigate: NavigateIcon,
40
56
  phone: PhoneIcon,
41
57
  search: SearchIcon,
58
+ settings: SettingsIcon,
42
59
  show: ShowIcon,
43
60
  user: UserIcon,
61
+ video: VideoIcon,
44
62
  } as const satisfies Record<string, IconComponent>;
45
63
 
46
64
  export type IconName = keyof typeof icons;
@@ -48,11 +66,23 @@ export type IconName = keyof typeof icons;
48
66
  export const iconNames = Object.keys(icons) as IconName[];
49
67
 
50
68
  export const iconGroups = {
51
- navigation: ["arrowRight", "chevronDown", "home", "navigate"],
52
- actions: ["show", "bell", "camera", "key", "check", "checkBadge", "search"],
53
- objects: ["bag", "building", "clock", "user"],
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
+ ],
82
+ objects: ["bag", "building", "clock", "user", "video"],
54
83
  communication: ["mail", "phone"],
55
84
  social: ["facebook", "instagram", "linkedin"],
85
+ interface: ["layoutGrid", "settings", "helpCircle", "logOut"],
56
86
  } as const satisfies Record<string, readonly IconName[]>;
57
87
 
58
88
  export type IconGroup = keyof typeof iconGroups;
package/src/index.ts CHANGED
@@ -1,10 +1,25 @@
1
- export { Button, Checkbox, Input, Select, Textarea, Toggle } from "./components";
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 {
12
+ AvatarProps,
13
+ AvatarMenuItem,
3
14
  ButtonProps,
4
15
  ButtonSize,
5
16
  ButtonVariant,
6
17
  CheckboxProps,
7
18
  CheckboxVariant,
19
+ DatePickerProps,
20
+ DatePickerSingleProps,
21
+ DatePickerRangeProps,
22
+ DateRange,
8
23
  InputProps,
9
24
  InputSize,
10
25
  SelectOption,
@@ -15,6 +30,9 @@ export type {
15
30
  ToggleProps,
16
31
  } from "./components";
17
32
 
33
+ export { MenuItem } from "./primitives";
34
+ export type { MenuItemProps } from "./primitives";
35
+
18
36
  export {
19
37
  Icon,
20
38
  icons,
@@ -26,29 +44,40 @@ export {
26
44
  BagIcon,
27
45
  BellIcon,
28
46
  BuildingIcon,
47
+ CalendarIcon,
48
+ CalendarOutlineIcon,
29
49
  CameraIcon,
30
50
  CheckIcon,
31
51
  CheckBadgeIcon,
32
52
  ChevronDownIcon,
53
+ ChevronLeftIcon,
33
54
  ClockIcon,
34
55
  FacebookIcon,
56
+ HeartIcon,
57
+ HelpCircleIcon,
35
58
  HomeIcon,
36
59
  InstagramIcon,
37
60
  KeyIcon,
61
+ LayoutGridIcon,
38
62
  LinkedInIcon,
63
+ LogOutIcon,
39
64
  MailIcon,
40
65
  NavigateIcon,
41
66
  PhoneIcon,
42
67
  SearchIcon,
68
+ SettingsIcon,
43
69
  ShowIcon,
44
70
  UserIcon,
71
+ VideoIcon,
45
72
  } from "./icons";
46
73
  export type { IconProps, IconName, IconGroup, IconComponent, IconByNameProps } from "./icons";
47
74
 
48
75
  export {
49
76
  colors,
50
77
  grey,
78
+ avatarTypography,
51
79
  buttonTypography,
80
+ datePickerTypography,
52
81
  inputTypography,
53
82
  selectTypography,
54
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
+ });
@@ -0,0 +1,2 @@
1
+ export { MenuItem } from "./MenuItem";
2
+ export type { MenuItemProps } from "./MenuItem";
@@ -0,0 +1,2 @@
1
+ export { MenuItem } from "./MenuItem";
2
+ export type { MenuItemProps } from "./MenuItem";
@@ -26,7 +26,9 @@ export const colors = {
26
26
  red: "#A63E3E",
27
27
  lightGrey: "#B7B7B7",
28
28
  primaryMuted: "#B464360F",
29
+ primaryHover: "#B4643633",
29
30
  redMuted: "#A63E3E0F",
31
+ redHover: "#A63E3E33",
30
32
 
31
33
  grey0: grey["0"],
32
34
  grey50: grey["0.5"],
@@ -3,7 +3,9 @@ export type { GreyStep } from "./colors";
3
3
 
4
4
  export {
5
5
  fonts,
6
+ avatarTypography,
6
7
  buttonTypography,
8
+ datePickerTypography,
7
9
  inputTypography,
8
10
  selectTypography,
9
11
  textareaTypography,
@@ -70,6 +70,71 @@ export const selectTypography = {
70
70
  },
71
71
  } as const;
72
72
 
73
+ /** Avatar name / email / menu-item typography — 100% line-height, 0 letter-spacing. */
74
+ export const avatarTypography = {
75
+ name: {
76
+ fontFamily: fonts.sans,
77
+ fontSize: 14,
78
+ fontWeight: "600" as const,
79
+ lineHeight: 14,
80
+ letterSpacing: 0,
81
+ },
82
+ email: {
83
+ fontFamily: fonts.sans,
84
+ fontSize: 12,
85
+ fontWeight: "400" as const,
86
+ lineHeight: 12,
87
+ letterSpacing: 0,
88
+ },
89
+ menuItem: {
90
+ fontFamily: fonts.sans,
91
+ fontSize: 12,
92
+ fontWeight: "600" as const,
93
+ lineHeight: 12,
94
+ letterSpacing: 0,
95
+ },
96
+ } as const;
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
+
73
138
  /** Textarea label / field / hint typography — Medium/Regular 12–14, letter-spacing 3%. */
74
139
  export const textareaTypography = {
75
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
+ }