@scripso-homepad/ui 0.4.1 → 0.4.3
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/{chunk-C7GHBVMM.js → chunk-66WR57JJ.js} +53 -10
- package/dist/chunk-66WR57JJ.js.map +1 -0
- package/dist/index.cjs +2297 -391
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -2
- package/dist/index.d.ts +144 -2
- package/dist/index.js +1978 -96
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +30411 -759
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.css +78 -0
- package/dist/web/index.css.map +1 -0
- package/dist/web/index.d.cts +267 -6
- package/dist/web/index.d.ts +267 -6
- package/dist/web/index.js +30179 -601
- package/dist/web/index.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Calendar.stories.tsx +337 -0
- package/src/components/Calendar.tsx +441 -0
- package/src/components/DatePicker.stories.tsx +219 -0
- package/src/components/DatePicker.tsx +366 -0
- package/src/components/Input.tsx +5 -1
- package/src/components/Select.stories.tsx +134 -0
- package/src/components/Select.tsx +462 -0
- package/src/css.d.ts +1 -0
- package/src/icons/CalendarIcon.tsx +28 -0
- package/src/icons/CalendarIcon.web.tsx +34 -0
- package/src/icons/ChevronDownIcon.tsx +11 -4
- package/src/icons/ChevronDownIcon.web.tsx +6 -4
- package/src/icons/ChevronNavIcons.tsx +44 -0
- package/src/icons/ChevronNavIcons.web.tsx +57 -0
- package/src/icons/LockIcon.tsx +30 -0
- package/src/icons/LockIcon.web.tsx +35 -0
- package/src/icons/calendarIconPaths.ts +2 -0
- package/src/icons/chevronDownPath.ts +1 -0
- package/src/icons/chevronNavPaths.ts +2 -0
- package/src/icons/lockIconPaths.ts +2 -0
- package/src/index.ts +43 -0
- package/src/theme/calendar.ts +357 -0
- package/src/theme/input.ts +16 -6
- package/src/theme/select.ts +249 -0
- package/src/utils/calendarDays.ts +273 -0
- package/src/utils/calendarLocaleContext.tsx +95 -0
- package/src/utils/calendarLocalization.ts +187 -0
- package/src/utils/countryDropdownScrollStyles.ts +53 -13
- package/src/utils/formatMultiSelectDisplay.ts +56 -0
- package/src/web/components/Badge.stories.tsx +43 -0
- package/src/web/components/Badge.tsx +35 -0
- package/src/web/components/Pagination.stories.tsx +78 -0
- package/src/web/components/Pagination.tsx +153 -0
- package/src/web/components/StatusBadge.tsx +20 -0
- package/src/web/components/Table.stories.tsx +415 -0
- package/src/web/components/Table.tsx +3 -0
- package/src/web/components/TableActionButton.tsx +67 -0
- package/src/web/components/TableActionsMenu.tsx +208 -0
- package/src/web/components/TableIconText.tsx +23 -0
- package/src/web/components/TableRowStatusActions.tsx +33 -0
- package/src/web/components/TableStatusActionsCell.tsx +29 -0
- package/src/web/components/pagination-utils.ts +39 -0
- package/src/web/components/table/DataTable.tsx +123 -0
- package/src/web/components/table/TablePrimitives.tsx +214 -0
- package/src/web/components/table/TableScrollArea.tsx +51 -0
- package/src/web/components/table/constants.ts +59 -0
- package/src/web/components/table/data-table-class-names.ts +26 -0
- package/src/web/components/table/index.ts +44 -0
- package/src/web/components/table/table-row-context.tsx +19 -0
- package/src/web/components/table/use-horizontal-scroll-state.ts +72 -0
- package/src/web/components/table-scroll.css +81 -0
- package/src/web/hooks/useOnClickOutside.ts +20 -5
- package/src/web/icons/TableMenuActivateIcon.tsx +27 -0
- package/src/web/icons/TableMenuCheckIcon.tsx +27 -0
- package/src/web/icons/TableMenuCloseIcon.tsx +27 -0
- package/src/web/icons/TableMenuEditIcon.tsx +27 -0
- package/src/web/icons/TableMenuInfoIcon.tsx +27 -0
- package/src/web/icons/TableMenuRefreshIcon.tsx +27 -0
- package/src/web/icons/TableMenuSuspendIcon.tsx +34 -0
- package/src/web/icons/TableMenuTrashIcon.tsx +27 -0
- package/src/web/icons/TableMoreIcon.tsx +41 -0
- package/src/web/icons/TableSortIcon.tsx +37 -0
- package/src/web/index.ts +74 -0
- package/src/web/layout/AppHeader.stories.tsx +2 -0
- package/src/web/layout/AppHeader.tsx +18 -14
- package/src/web/layout/DashboardLayout.stories.tsx +1 -0
- package/src/web/layout/DashboardLayout.tsx +8 -4
- package/src/web/layout/story-helpers.tsx +10 -2
- package/dist/chunk-C7GHBVMM.js.map +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Dimensions, Platform, StyleSheet, type TextStyle, type ViewStyle } from "react-native";
|
|
2
|
+
import {
|
|
3
|
+
resolveInputVisualState,
|
|
4
|
+
type InputStateFlags,
|
|
5
|
+
type InputVisualState,
|
|
6
|
+
} from "./input";
|
|
7
|
+
import { colors, fonts, fontSize, fontWeight, radii, spacing } from "./tokens";
|
|
8
|
+
|
|
9
|
+
export const SELECT_HEIGHT = 52;
|
|
10
|
+
export const SELECT_PADDING = 16;
|
|
11
|
+
export const SELECT_INNER_GAP = 10;
|
|
12
|
+
export const SELECT_ICON_SIZE = 20;
|
|
13
|
+
export const SELECT_LABEL_GAP = spacing.sm;
|
|
14
|
+
export const SELECT_DROPDOWN_GAP = spacing.sm;
|
|
15
|
+
export const SELECT_DROPDOWN_PADDING = spacing.sm;
|
|
16
|
+
export const SELECT_DROPDOWN_MAX_HEIGHT = 317;
|
|
17
|
+
export const SELECT_OPTION_HEIGHT = 43;
|
|
18
|
+
export const SELECT_OPTION_GAP = spacing.sm;
|
|
19
|
+
export const SELECT_CHEVRON_SIZE = 20;
|
|
20
|
+
export const SELECT_CHEVRON_COLOR = colors.stormGray150;
|
|
21
|
+
export const SELECT_CHEVRON_ROTATION_MS = 200;
|
|
22
|
+
|
|
23
|
+
export { resolveInputVisualState };
|
|
24
|
+
export type { InputStateFlags, InputVisualState };
|
|
25
|
+
|
|
26
|
+
export const selectLabelTypography = {
|
|
27
|
+
fontFamily: fonts.sans,
|
|
28
|
+
fontSize: fontSize.sm,
|
|
29
|
+
fontWeight: fontWeight.medium,
|
|
30
|
+
lineHeight: fontSize.sm,
|
|
31
|
+
letterSpacing: 0,
|
|
32
|
+
color: colors.slateBlue,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export const selectValueTypography = {
|
|
36
|
+
fontFamily: fonts.sans,
|
|
37
|
+
fontSize: fontSize.md,
|
|
38
|
+
fontWeight: fontWeight.regular,
|
|
39
|
+
lineHeight: fontSize.md,
|
|
40
|
+
letterSpacing: 0,
|
|
41
|
+
textAlign: "left",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
interface SelectFieldStyleSet {
|
|
45
|
+
container: ViewStyle;
|
|
46
|
+
outline: ViewStyle;
|
|
47
|
+
value: TextStyle;
|
|
48
|
+
placeholder: string;
|
|
49
|
+
icon: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const INPUT_OUTLINE_WIDTH = 2;
|
|
53
|
+
|
|
54
|
+
function createOutlineStyle(ringColor: string): ViewStyle {
|
|
55
|
+
return {
|
|
56
|
+
borderRadius: radii.lg + INPUT_OUTLINE_WIDTH,
|
|
57
|
+
borderWidth: INPUT_OUTLINE_WIDTH,
|
|
58
|
+
borderColor: ringColor,
|
|
59
|
+
padding: 0,
|
|
60
|
+
backgroundColor: colors.transparent,
|
|
61
|
+
width: "100%",
|
|
62
|
+
alignSelf: "stretch",
|
|
63
|
+
...(Platform.OS !== "web" ? { overflow: "hidden" as const } : null),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const defaultOutline = createOutlineStyle(colors.transparent);
|
|
68
|
+
|
|
69
|
+
export function getSelectFieldStyles(state: InputVisualState): SelectFieldStyleSet {
|
|
70
|
+
const containerBase: ViewStyle = {
|
|
71
|
+
borderRadius: radii.lg,
|
|
72
|
+
...(Platform.OS !== "web" ? { overflow: "hidden" as const } : null),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const valueBase: TextStyle = {
|
|
76
|
+
...selectValueTypography,
|
|
77
|
+
color: colors.slateBlue,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
switch (state) {
|
|
81
|
+
case "disabled":
|
|
82
|
+
return {
|
|
83
|
+
outline: defaultOutline,
|
|
84
|
+
container: {
|
|
85
|
+
...containerBase,
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: colors.stormGray50,
|
|
88
|
+
backgroundColor: colors.stormGray50,
|
|
89
|
+
},
|
|
90
|
+
value: { ...valueBase, color: colors.stormGray300 },
|
|
91
|
+
placeholder: colors.stormGray300,
|
|
92
|
+
icon: colors.stormGray150,
|
|
93
|
+
};
|
|
94
|
+
case "error":
|
|
95
|
+
return {
|
|
96
|
+
outline: createOutlineStyle(colors.inputOutlineError),
|
|
97
|
+
container: {
|
|
98
|
+
...containerBase,
|
|
99
|
+
borderWidth: 1,
|
|
100
|
+
borderColor: colors.inputError,
|
|
101
|
+
backgroundColor: colors.white,
|
|
102
|
+
},
|
|
103
|
+
value: { ...valueBase, color: colors.inputError },
|
|
104
|
+
placeholder: colors.stormGray200,
|
|
105
|
+
icon: colors.inputError,
|
|
106
|
+
};
|
|
107
|
+
case "focused":
|
|
108
|
+
return {
|
|
109
|
+
outline: createOutlineStyle(colors.inputOutlineFocus),
|
|
110
|
+
container: {
|
|
111
|
+
...containerBase,
|
|
112
|
+
borderWidth: 1,
|
|
113
|
+
borderColor: colors.navy,
|
|
114
|
+
backgroundColor: colors.white,
|
|
115
|
+
},
|
|
116
|
+
value: valueBase,
|
|
117
|
+
placeholder: colors.stormGray200,
|
|
118
|
+
icon: colors.navy,
|
|
119
|
+
};
|
|
120
|
+
default:
|
|
121
|
+
return {
|
|
122
|
+
outline: defaultOutline,
|
|
123
|
+
container: {
|
|
124
|
+
...containerBase,
|
|
125
|
+
borderWidth: 1,
|
|
126
|
+
borderColor: colors.stormGray50,
|
|
127
|
+
backgroundColor: colors.white,
|
|
128
|
+
},
|
|
129
|
+
value: valueBase,
|
|
130
|
+
placeholder: colors.stormGray200,
|
|
131
|
+
icon: colors.stormGray150,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const selectFieldMetrics = StyleSheet.create({
|
|
137
|
+
container: {
|
|
138
|
+
flexDirection: "row",
|
|
139
|
+
alignItems: "center",
|
|
140
|
+
height: SELECT_HEIGHT,
|
|
141
|
+
minHeight: SELECT_HEIGHT,
|
|
142
|
+
maxHeight: SELECT_HEIGHT,
|
|
143
|
+
borderRadius: radii.lg,
|
|
144
|
+
padding: SELECT_PADDING,
|
|
145
|
+
gap: SELECT_INNER_GAP,
|
|
146
|
+
...(Platform.OS === "web"
|
|
147
|
+
? ({
|
|
148
|
+
boxSizing: "border-box",
|
|
149
|
+
} as ViewStyle)
|
|
150
|
+
: null),
|
|
151
|
+
},
|
|
152
|
+
value: {
|
|
153
|
+
flex: 1,
|
|
154
|
+
minWidth: 0,
|
|
155
|
+
...selectValueTypography,
|
|
156
|
+
paddingVertical: 0,
|
|
157
|
+
paddingHorizontal: 0,
|
|
158
|
+
margin: 0,
|
|
159
|
+
...(Platform.OS === "android" ? { includeFontPadding: false } : null),
|
|
160
|
+
...(Platform.OS === "web"
|
|
161
|
+
? ({
|
|
162
|
+
outlineStyle: "none",
|
|
163
|
+
} as TextStyle)
|
|
164
|
+
: null),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const selectDropdownMetrics = StyleSheet.create({
|
|
169
|
+
menu: {
|
|
170
|
+
borderRadius: radii.lg,
|
|
171
|
+
borderWidth: 1,
|
|
172
|
+
borderColor: colors.stormGray50,
|
|
173
|
+
backgroundColor: colors.white,
|
|
174
|
+
padding: SELECT_DROPDOWN_PADDING,
|
|
175
|
+
overflow: "hidden",
|
|
176
|
+
},
|
|
177
|
+
option: {
|
|
178
|
+
width: "100%",
|
|
179
|
+
height: SELECT_OPTION_HEIGHT,
|
|
180
|
+
minHeight: SELECT_OPTION_HEIGHT,
|
|
181
|
+
maxHeight: SELECT_OPTION_HEIGHT,
|
|
182
|
+
paddingTop: 12,
|
|
183
|
+
paddingRight: spacing.sm,
|
|
184
|
+
paddingBottom: 12,
|
|
185
|
+
paddingLeft: spacing.sm,
|
|
186
|
+
justifyContent: "center",
|
|
187
|
+
alignItems: "flex-start",
|
|
188
|
+
},
|
|
189
|
+
optionLabel: {
|
|
190
|
+
fontFamily: fonts.sans,
|
|
191
|
+
fontSize: fontSize.md,
|
|
192
|
+
fontWeight: fontWeight.regular,
|
|
193
|
+
lineHeight: fontSize.md,
|
|
194
|
+
letterSpacing: 0,
|
|
195
|
+
textAlign: "left",
|
|
196
|
+
color: colors.slateBlue,
|
|
197
|
+
width: "100%",
|
|
198
|
+
},
|
|
199
|
+
optionLabelSelected: {
|
|
200
|
+
fontWeight: fontWeight.medium,
|
|
201
|
+
color: colors.slateBlue,
|
|
202
|
+
},
|
|
203
|
+
optionSelected: {
|
|
204
|
+
opacity: 1,
|
|
205
|
+
borderRadius: radii.sm,
|
|
206
|
+
paddingTop: 12,
|
|
207
|
+
paddingRight: spacing.sm,
|
|
208
|
+
paddingBottom: 12,
|
|
209
|
+
paddingLeft: spacing.sm,
|
|
210
|
+
backgroundColor: colors.countrySelectorSelectedBg,
|
|
211
|
+
},
|
|
212
|
+
optionHighlighted: {
|
|
213
|
+
backgroundColor: colors.stormGray0,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export function resolveSelectDropdownHeight(optionCount: number): number {
|
|
218
|
+
const contentHeight =
|
|
219
|
+
optionCount * SELECT_OPTION_HEIGHT +
|
|
220
|
+
Math.max(0, optionCount - 1) * SELECT_OPTION_GAP +
|
|
221
|
+
SELECT_DROPDOWN_PADDING * 2;
|
|
222
|
+
|
|
223
|
+
return Math.min(contentHeight, SELECT_DROPDOWN_MAX_HEIGHT);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resolveSelectDropdownTop(
|
|
227
|
+
anchor: { y: number; height: number },
|
|
228
|
+
dropdownHeight: number,
|
|
229
|
+
): number {
|
|
230
|
+
const windowHeight = Dimensions.get("window").height;
|
|
231
|
+
const spaceBelow = windowHeight - (anchor.y + anchor.height + SELECT_DROPDOWN_GAP);
|
|
232
|
+
const spaceAbove = anchor.y - SELECT_DROPDOWN_GAP;
|
|
233
|
+
const topBelow = anchor.y + anchor.height + SELECT_DROPDOWN_GAP;
|
|
234
|
+
const topAbove = anchor.y - SELECT_DROPDOWN_GAP - dropdownHeight;
|
|
235
|
+
|
|
236
|
+
if (spaceBelow >= dropdownHeight) {
|
|
237
|
+
return topBelow;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (spaceAbove >= dropdownHeight) {
|
|
241
|
+
return topAbove;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (spaceAbove > spaceBelow) {
|
|
245
|
+
return Math.max(0, topAbove);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Math.max(0, Math.min(topBelow, windowHeight - dropdownHeight));
|
|
249
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
export function stripTime(date: Date): Date {
|
|
2
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type DateRange = {
|
|
6
|
+
start: Date;
|
|
7
|
+
end?: Date;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function normalizeDateRange(range?: DateRange): { start?: Date; end?: Date } {
|
|
11
|
+
if (!range?.start) {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const start = stripTime(range.start);
|
|
16
|
+
|
|
17
|
+
if (!range.end) {
|
|
18
|
+
return { start };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const end = stripTime(range.end);
|
|
22
|
+
|
|
23
|
+
if (isBeforeDay(end, start)) {
|
|
24
|
+
return { start: end, end: start };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { start, end };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isInDateRange(date: Date, start?: Date, end?: Date): boolean {
|
|
31
|
+
if (!start) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const day = stripTime(date);
|
|
36
|
+
|
|
37
|
+
if (!end) {
|
|
38
|
+
return isSameDay(day, start);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return !isBeforeDay(day, start) && !isAfterDay(day, end);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function selectRangeDate(clicked: Date, current?: DateRange): DateRange {
|
|
45
|
+
const clickedDate = stripTime(clicked);
|
|
46
|
+
|
|
47
|
+
if (!current?.start || current.end) {
|
|
48
|
+
return { start: clickedDate };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const start = stripTime(current.start);
|
|
52
|
+
|
|
53
|
+
if (isSameDay(clickedDate, start)) {
|
|
54
|
+
return { start, end: clickedDate };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isBeforeDay(clickedDate, start)) {
|
|
58
|
+
return { start: clickedDate };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { start, end: clickedDate };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isSameDay(left: Date, right: Date): boolean {
|
|
65
|
+
return (
|
|
66
|
+
left.getFullYear() === right.getFullYear() &&
|
|
67
|
+
left.getMonth() === right.getMonth() &&
|
|
68
|
+
left.getDate() === right.getDate()
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isBeforeDay(left: Date, right: Date): boolean {
|
|
73
|
+
return stripTime(left).getTime() < stripTime(right).getTime();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isAfterDay(left: Date, right: Date): boolean {
|
|
77
|
+
return stripTime(left).getTime() > stripTime(right).getTime();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function addMonths(date: Date, amount: number): Date {
|
|
81
|
+
return new Date(date.getFullYear(), date.getMonth() + amount, 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isDateDisabled(
|
|
85
|
+
date: Date,
|
|
86
|
+
minDate?: Date,
|
|
87
|
+
maxDate?: Date,
|
|
88
|
+
): boolean {
|
|
89
|
+
if (minDate && isBeforeDay(date, minDate)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (maxDate && isAfterDay(date, maxDate)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type CalendarDayCell = {
|
|
101
|
+
date: Date;
|
|
102
|
+
inCurrentMonth: boolean;
|
|
103
|
+
isToday: boolean;
|
|
104
|
+
isSelected: boolean;
|
|
105
|
+
isRangeStart: boolean;
|
|
106
|
+
isRangeEnd: boolean;
|
|
107
|
+
isInRange: boolean;
|
|
108
|
+
isPast: boolean;
|
|
109
|
+
isDisabled: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const CALENDAR_VISIBLE_DAY_COUNT = 42;
|
|
113
|
+
const MONDAY_WEEK_START = 1;
|
|
114
|
+
|
|
115
|
+
export function buildCalendarDays({
|
|
116
|
+
viewDate,
|
|
117
|
+
selectedDate,
|
|
118
|
+
selectedRange,
|
|
119
|
+
today,
|
|
120
|
+
minDate,
|
|
121
|
+
maxDate,
|
|
122
|
+
}: {
|
|
123
|
+
viewDate: Date;
|
|
124
|
+
selectedDate?: Date;
|
|
125
|
+
selectedRange?: DateRange;
|
|
126
|
+
today: Date;
|
|
127
|
+
minDate?: Date;
|
|
128
|
+
maxDate?: Date;
|
|
129
|
+
}): CalendarDayCell[] {
|
|
130
|
+
const { start: rangeStart, end: rangeEnd } = normalizeDateRange(selectedRange);
|
|
131
|
+
const year = viewDate.getFullYear();
|
|
132
|
+
const month = viewDate.getMonth();
|
|
133
|
+
const firstOfMonth = new Date(year, month, 1);
|
|
134
|
+
const startOffset = (firstOfMonth.getDay() - MONDAY_WEEK_START + 7) % 7;
|
|
135
|
+
const gridStart = new Date(year, month, 1 - startOffset);
|
|
136
|
+
const days: CalendarDayCell[] = [];
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < CALENDAR_VISIBLE_DAY_COUNT; index += 1) {
|
|
139
|
+
const date = new Date(
|
|
140
|
+
gridStart.getFullYear(),
|
|
141
|
+
gridStart.getMonth(),
|
|
142
|
+
gridStart.getDate() + index,
|
|
143
|
+
);
|
|
144
|
+
const inCurrentMonth = date.getMonth() === month;
|
|
145
|
+
const isPast = inCurrentMonth && isBeforeDay(date, today);
|
|
146
|
+
const isDisabled = isDateDisabled(date, minDate, maxDate);
|
|
147
|
+
const isRangeStart = rangeStart ? isSameDay(date, rangeStart) : false;
|
|
148
|
+
const isRangeEnd = rangeEnd ? isSameDay(date, rangeEnd) : false;
|
|
149
|
+
const inSelectedRange =
|
|
150
|
+
rangeStart && rangeEnd ? isInDateRange(date, rangeStart, rangeEnd) : false;
|
|
151
|
+
const isInRange = inSelectedRange && !isRangeStart && !isRangeEnd;
|
|
152
|
+
const isSelected = selectedDate
|
|
153
|
+
? isSameDay(date, selectedDate)
|
|
154
|
+
: inSelectedRange || Boolean(rangeStart && !rangeEnd && isRangeStart);
|
|
155
|
+
|
|
156
|
+
days.push({
|
|
157
|
+
date,
|
|
158
|
+
inCurrentMonth,
|
|
159
|
+
isToday: isSameDay(date, today),
|
|
160
|
+
isSelected,
|
|
161
|
+
isRangeStart,
|
|
162
|
+
isRangeEnd,
|
|
163
|
+
isInRange,
|
|
164
|
+
isPast,
|
|
165
|
+
isDisabled,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return days;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatCalendarMonthYear(date: Date, locale: string): string {
|
|
173
|
+
const formatted = new Intl.DateTimeFormat(locale, {
|
|
174
|
+
month: "short",
|
|
175
|
+
year: "numeric",
|
|
176
|
+
}).format(date);
|
|
177
|
+
|
|
178
|
+
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatCalendarYear(year: number, locale: string): string {
|
|
182
|
+
return new Intl.DateTimeFormat(locale, { year: "numeric" }).format(new Date(year, 0, 1));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getMonthLabels(locale: string): string[] {
|
|
186
|
+
return Array.from({ length: 12 }, (_, month) => {
|
|
187
|
+
const label = new Intl.DateTimeFormat(locale, { month: "short" }).format(
|
|
188
|
+
new Date(2024, month, 1),
|
|
189
|
+
);
|
|
190
|
+
const normalized = label.replace(/\./g, "");
|
|
191
|
+
|
|
192
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getYearPageStart(year: number, yearsPerPage = 12): number {
|
|
197
|
+
return Math.floor(year / yearsPerPage) * yearsPerPage;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function isMonthDisabled(
|
|
201
|
+
year: number,
|
|
202
|
+
month: number,
|
|
203
|
+
minDate?: Date,
|
|
204
|
+
maxDate?: Date,
|
|
205
|
+
): boolean {
|
|
206
|
+
const firstDay = new Date(year, month, 1);
|
|
207
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
208
|
+
|
|
209
|
+
if (minDate && isBeforeDay(lastDay, minDate)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (maxDate && isAfterDay(firstDay, maxDate)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function isYearDisabled(year: number, minDate?: Date, maxDate?: Date): boolean {
|
|
221
|
+
const firstDay = new Date(year, 0, 1);
|
|
222
|
+
const lastDay = new Date(year, 11, 31);
|
|
223
|
+
|
|
224
|
+
if (minDate && isBeforeDay(lastDay, minDate)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (maxDate && isAfterDay(firstDay, maxDate)) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function formatCalendarDate(date: Date, locale: string): string {
|
|
236
|
+
return new Intl.DateTimeFormat(locale, {
|
|
237
|
+
day: "numeric",
|
|
238
|
+
month: "long",
|
|
239
|
+
year: "numeric",
|
|
240
|
+
}).format(date);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function formatCalendarDateRange(
|
|
244
|
+
range: DateRange,
|
|
245
|
+
locale: string,
|
|
246
|
+
separator = " – ",
|
|
247
|
+
): string {
|
|
248
|
+
const { start, end } = normalizeDateRange(range);
|
|
249
|
+
|
|
250
|
+
if (!start) {
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!end) {
|
|
255
|
+
return formatCalendarDate(start, locale);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return `${formatCalendarDate(start, locale)}${separator}${formatCalendarDate(end, locale)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** @deprecated Use `resolveWeekdayLabels` from `calendarLocalization`; calendars are Monday-first. */
|
|
262
|
+
export function getDefaultWeekdayLabels(locale: string): string[] {
|
|
263
|
+
const labels: string[] = [];
|
|
264
|
+
const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
|
|
265
|
+
|
|
266
|
+
for (let index = 0; index < 7; index += 1) {
|
|
267
|
+
const day = new Date(2024, 0, index + 1);
|
|
268
|
+
const label = formatter.format(day).replace(/\./g, "").toUpperCase();
|
|
269
|
+
labels.push(label.slice(0, 2));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return labels;
|
|
273
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import { resolveCalendarLocale, type CalendarLocale } from "./calendarLocalization";
|
|
3
|
+
|
|
4
|
+
const LOCALE_STORAGE_KEY = "homepad.locale";
|
|
5
|
+
const LOCALE_CHANGE_EVENT = "homepad:locale-change";
|
|
6
|
+
const DEFAULT_CALENDAR_LOCALE: CalendarLocale = "hy-AM";
|
|
7
|
+
|
|
8
|
+
const CalendarLocaleContext = createContext<string | undefined>(undefined);
|
|
9
|
+
|
|
10
|
+
export type CalendarLocaleProviderProps = {
|
|
11
|
+
locale: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function CalendarLocaleProvider({ locale, children }: CalendarLocaleProviderProps) {
|
|
16
|
+
return (
|
|
17
|
+
<CalendarLocaleContext.Provider value={locale}>{children}</CalendarLocaleContext.Provider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeProjectLocale(locale: string | null | undefined): CalendarLocale | null {
|
|
22
|
+
if (!locale) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return resolveCalendarLocale(locale);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readBrowserLocale(): CalendarLocale | null {
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let storedLocale: CalendarLocale | null = null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
storedLocale = normalizeProjectLocale(window.localStorage.getItem(LOCALE_STORAGE_KEY));
|
|
38
|
+
} catch {
|
|
39
|
+
storedLocale = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (storedLocale) {
|
|
43
|
+
return storedLocale;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const documentLocale = normalizeProjectLocale(window.document?.documentElement.lang);
|
|
47
|
+
if (documentLocale) {
|
|
48
|
+
return documentLocale;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return normalizeProjectLocale(window.navigator.language);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useCalendarLocale(locale?: string): CalendarLocale {
|
|
55
|
+
const contextLocale = useContext(CalendarLocaleContext);
|
|
56
|
+
const controlledLocale = locale ?? contextLocale;
|
|
57
|
+
const [browserLocale, setBrowserLocale] = useState<CalendarLocale>(() =>
|
|
58
|
+
readBrowserLocale() ?? DEFAULT_CALENDAR_LOCALE,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (controlledLocale !== undefined || typeof window === "undefined") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const syncLocale = () => {
|
|
67
|
+
setBrowserLocale(readBrowserLocale() ?? DEFAULT_CALENDAR_LOCALE);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleLocaleChange = (event: Event) => {
|
|
71
|
+
const customEvent = event as CustomEvent<string>;
|
|
72
|
+
const nextLocale = normalizeProjectLocale(customEvent.detail);
|
|
73
|
+
|
|
74
|
+
if (nextLocale) {
|
|
75
|
+
setBrowserLocale(nextLocale);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
syncLocale();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
window.addEventListener(LOCALE_CHANGE_EVENT, handleLocaleChange);
|
|
83
|
+
window.addEventListener("storage", syncLocale);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener(LOCALE_CHANGE_EVENT, handleLocaleChange);
|
|
87
|
+
window.removeEventListener("storage", syncLocale);
|
|
88
|
+
};
|
|
89
|
+
}, [controlledLocale]);
|
|
90
|
+
|
|
91
|
+
return useMemo(
|
|
92
|
+
() => normalizeProjectLocale(controlledLocale) ?? browserLocale,
|
|
93
|
+
[browserLocale, controlledLocale],
|
|
94
|
+
);
|
|
95
|
+
}
|