@helpwave/hightide 0.0.1
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/.storybook/main.ts +24 -0
- package/.storybook/preview.tsx +67 -0
- package/LICENSE +373 -0
- package/README.md +8 -0
- package/coloring/shading.ts +46 -0
- package/coloring/types.ts +13 -0
- package/components/Avatar.tsx +58 -0
- package/components/AvatarGroup.tsx +48 -0
- package/components/BreadCrumb.tsx +35 -0
- package/components/Button.tsx +236 -0
- package/components/ChipList.tsx +89 -0
- package/components/Circle.tsx +27 -0
- package/components/ErrorComponent.tsx +40 -0
- package/components/Expandable.tsx +61 -0
- package/components/HelpwaveBadge.tsx +35 -0
- package/components/HideableContentSection.tsx +43 -0
- package/components/InputGroup.tsx +72 -0
- package/components/LoadingAndErrorComponent.tsx +47 -0
- package/components/LoadingAnimation.tsx +40 -0
- package/components/LoadingButton.tsx +27 -0
- package/components/MarkdownInterpreter.tsx +278 -0
- package/components/Pagination.tsx +65 -0
- package/components/Profile.tsx +124 -0
- package/components/ProgressIndicator.tsx +58 -0
- package/components/Ring.tsx +286 -0
- package/components/SearchableList.tsx +69 -0
- package/components/SortButton.tsx +33 -0
- package/components/Span.tsx +0 -0
- package/components/StepperBar.tsx +124 -0
- package/components/Table.tsx +330 -0
- package/components/TechRadar.tsx +247 -0
- package/components/TextImage.tsx +86 -0
- package/components/TimeDisplay.tsx +121 -0
- package/components/Tooltip.tsx +92 -0
- package/components/VerticalDivider.tsx +51 -0
- package/components/date/DatePicker.tsx +164 -0
- package/components/date/DayPicker.tsx +95 -0
- package/components/date/TimePicker.tsx +167 -0
- package/components/date/YearMonthPicker.tsx +130 -0
- package/components/examples/InputGroupExample.tsx +58 -0
- package/components/examples/MultiSelectExample.tsx +57 -0
- package/components/examples/SearchableSelectExample.tsx +34 -0
- package/components/examples/SelectExample.tsx +28 -0
- package/components/examples/StackingModals.tsx +54 -0
- package/components/examples/TableExample.tsx +159 -0
- package/components/examples/TextareaExample.tsx +23 -0
- package/components/examples/TileExample.tsx +25 -0
- package/components/examples/Title.tsx +0 -0
- package/components/examples/date/DateTimePickerExample.tsx +53 -0
- package/components/examples/properties/CheckboxPropertyExample.tsx +29 -0
- package/components/examples/properties/DatePropertyExample.tsx +44 -0
- package/components/examples/properties/MultiSelectPropertyExample.tsx +39 -0
- package/components/examples/properties/NumberPropertyExample.tsx +28 -0
- package/components/examples/properties/SelectPropertyExample.tsx +39 -0
- package/components/examples/properties/TextPropertyExample.tsx +30 -0
- package/components/icons/Helpwave.tsx +51 -0
- package/components/icons/Tag.tsx +29 -0
- package/components/layout/Carousel.tsx +396 -0
- package/components/layout/DividerInserter.tsx +37 -0
- package/components/layout/FAQSection.tsx +57 -0
- package/components/layout/Tile.tsx +67 -0
- package/components/modals/ConfirmDialog.tsx +105 -0
- package/components/modals/DiscardChangesDialog.tsx +71 -0
- package/components/modals/InputModal.tsx +26 -0
- package/components/modals/LanguageModal.tsx +76 -0
- package/components/modals/Modal.tsx +149 -0
- package/components/modals/ModalRegister.tsx +45 -0
- package/components/properties/CheckboxProperty.tsx +62 -0
- package/components/properties/DateProperty.tsx +58 -0
- package/components/properties/MultiSelectProperty.tsx +82 -0
- package/components/properties/NumberProperty.tsx +86 -0
- package/components/properties/PropertyBase.tsx +84 -0
- package/components/properties/SelectProperty.tsx +67 -0
- package/components/properties/TextProperty.tsx +81 -0
- package/components/user-input/Checkbox.tsx +139 -0
- package/components/user-input/DateAndTimePicker.tsx +156 -0
- package/components/user-input/Input.tsx +192 -0
- package/components/user-input/Label.tsx +32 -0
- package/components/user-input/Menu.tsx +75 -0
- package/components/user-input/MultiSelect.tsx +158 -0
- package/components/user-input/ScrollPicker.tsx +240 -0
- package/components/user-input/SearchableSelect.tsx +36 -0
- package/components/user-input/Select.tsx +132 -0
- package/components/user-input/Textarea.tsx +86 -0
- package/components/user-input/ToggleableInput.tsx +115 -0
- package/eslint.config.js +3 -0
- package/globals.css +488 -0
- package/hooks/useHoverState.ts +88 -0
- package/hooks/useLanguage.tsx +78 -0
- package/hooks/useLocalStorage.tsx +33 -0
- package/hooks/useOutsideClick.ts +25 -0
- package/hooks/useSaveDelay.ts +46 -0
- package/hooks/useTheme.tsx +57 -0
- package/hooks/useTranslation.ts +43 -0
- package/index.ts +0 -0
- package/package.json +71 -0
- package/postcss.config.mjs +7 -0
- package/stories/README.md +23 -0
- package/stories/coloring/shading.stories.tsx +54 -0
- package/stories/geometry/Circle.stories.tsx +16 -0
- package/stories/geometry/rings/AnimatedRing.stories.tsx +18 -0
- package/stories/geometry/rings/RadialRings.stories.tsx +19 -0
- package/stories/geometry/rings/Ring.stories.tsx +17 -0
- package/stories/geometry/rings/RingWave.stories.tsx +20 -0
- package/stories/layout/FAQSection.stories.tsx +49 -0
- package/stories/layout/InputGroup.stories.tsx +19 -0
- package/stories/layout/Table.stories.tsx +19 -0
- package/stories/layout/TextImage.stories.tsx +24 -0
- package/stories/layout/chip/Chip.stories.tsx +19 -0
- package/stories/layout/chip/ChipList.stories.tsx +27 -0
- package/stories/layout/tile/Tile.stories.ts +20 -0
- package/stories/layout/tile/TileWithImage.stories.tsx +27 -0
- package/stories/other/BreadCrumbs.stories.tsx +21 -0
- package/stories/other/HelpwaveBadge.stories.tsx +18 -0
- package/stories/other/HelpwaveSpinner.stories.tsx +19 -0
- package/stories/other/MarkdownInterpreter.stories.tsx +18 -0
- package/stories/other/Profile.stories.tsx +52 -0
- package/stories/other/SearchableList.stories.tsx +21 -0
- package/stories/other/StackingModals.stories.tsx +16 -0
- package/stories/other/TechRadar.stories.tsx +14 -0
- package/stories/other/Translation.stories.tsx +56 -0
- package/stories/other/VerticalDivider.stories.tsx +20 -0
- package/stories/other/avatar/Avatar.stories.tsx +19 -0
- package/stories/other/avatar/AvatarGroup.stories.tsx +26 -0
- package/stories/other/tooltip/Tooltip.stories.tsx +30 -0
- package/stories/other/tooltip/TooltipStack.stories.tsx +39 -0
- package/stories/user-action/button/LoadingButton.stories.tsx +21 -0
- package/stories/user-action/button/OutlineButton.stories.tsx +22 -0
- package/stories/user-action/button/SolidButton.stories.tsx +22 -0
- package/stories/user-action/button/TextButton.stories.tsx +22 -0
- package/stories/user-action/input/Checkbox.stories.tsx +20 -0
- package/stories/user-action/input/Label.stories.tsx +18 -0
- package/stories/user-action/input/ScrollPicker.stories.tsx +20 -0
- package/stories/user-action/input/Textarea.stories.tsx +22 -0
- package/stories/user-action/input/date/DatePicker.stories.tsx +23 -0
- package/stories/user-action/input/date/DateTimePicker.stories.tsx +26 -0
- package/stories/user-action/input/date/DayPicker.stories.tsx +20 -0
- package/stories/user-action/input/date/TimePicker.stories.tsx +20 -0
- package/stories/user-action/input/date/YearMonthPicker.stories.tsx +21 -0
- package/stories/user-action/input/select/MultiSelect.stories.tsx +39 -0
- package/stories/user-action/input/select/SearchableSelect.stories.tsx +32 -0
- package/stories/user-action/input/select/Select.stories.tsx +30 -0
- package/stories/user-action/properties/CheckboxProperty.stories.tsx +20 -0
- package/stories/user-action/properties/DateProperty.stories.tsx +21 -0
- package/stories/user-action/properties/MultiSelectProperty.stories.tsx +33 -0
- package/stories/user-action/properties/NumberProperty.stories.tsx +21 -0
- package/stories/user-action/properties/PropertyBase.stories.tsx +28 -0
- package/stories/user-action/properties/SingleSelectProperty.stories.tsx +35 -0
- package/stories/user-action/properties/TextProperty.stories.tsx +20 -0
- package/tsconfig.json +20 -0
- package/util/array.ts +115 -0
- package/util/builder.ts +9 -0
- package/util/date.ts +180 -0
- package/util/easeFunctions.ts +37 -0
- package/util/emailValidation.ts +3 -0
- package/util/loopingArray.ts +94 -0
- package/util/math.ts +3 -0
- package/util/news.ts +43 -0
- package/util/noop.ts +1 -0
- package/util/simpleSearch.ts +65 -0
- package/util/storage.ts +37 -0
- package/util/types.ts +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Languages } from '../hooks/useLanguage'
|
|
2
|
+
import type { PropsForTranslation } from '../hooks/useTranslation'
|
|
3
|
+
import { useTranslation } from '../hooks/useTranslation'
|
|
4
|
+
|
|
5
|
+
type TimeDisplayTranslation = {
|
|
6
|
+
today: string,
|
|
7
|
+
yesterday: string,
|
|
8
|
+
tomorrow: string,
|
|
9
|
+
inDays: (days: number) => string,
|
|
10
|
+
agoDays: (days: number) => string,
|
|
11
|
+
january: string,
|
|
12
|
+
february: string,
|
|
13
|
+
march: string,
|
|
14
|
+
april: string,
|
|
15
|
+
may: string,
|
|
16
|
+
june: string,
|
|
17
|
+
july: string,
|
|
18
|
+
august: string,
|
|
19
|
+
september: string,
|
|
20
|
+
october: string,
|
|
21
|
+
november: string,
|
|
22
|
+
december: string,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultTimeDisplayTranslations: Record<Languages, TimeDisplayTranslation> = {
|
|
26
|
+
en: {
|
|
27
|
+
today: 'today',
|
|
28
|
+
yesterday: 'yesterday',
|
|
29
|
+
tomorrow: 'tomorrow',
|
|
30
|
+
inDays: (days: number) => `in ${days} days`,
|
|
31
|
+
agoDays: (days: number) => `${days} days ago`,
|
|
32
|
+
january: 'January',
|
|
33
|
+
february: 'February',
|
|
34
|
+
march: 'March',
|
|
35
|
+
april: 'April',
|
|
36
|
+
may: 'May',
|
|
37
|
+
june: 'June',
|
|
38
|
+
july: 'July',
|
|
39
|
+
august: 'August',
|
|
40
|
+
september: 'September',
|
|
41
|
+
october: 'October',
|
|
42
|
+
november: 'November',
|
|
43
|
+
december: 'December'
|
|
44
|
+
},
|
|
45
|
+
de: {
|
|
46
|
+
today: 'heute',
|
|
47
|
+
yesterday: 'gestern',
|
|
48
|
+
tomorrow: 'morgen',
|
|
49
|
+
inDays: (days: number) => `in ${days} Tagen`,
|
|
50
|
+
agoDays: (days: number) => `vor ${days} Tagen`,
|
|
51
|
+
january: 'Januar',
|
|
52
|
+
february: 'Februar',
|
|
53
|
+
march: 'März',
|
|
54
|
+
april: 'April',
|
|
55
|
+
may: 'Mai',
|
|
56
|
+
june: 'Juni',
|
|
57
|
+
july: 'Juli',
|
|
58
|
+
august: 'August',
|
|
59
|
+
september: 'September',
|
|
60
|
+
october: 'October',
|
|
61
|
+
november: 'November',
|
|
62
|
+
december: 'December'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type TimeDisplayMode = 'daysFromToday' | 'date'
|
|
67
|
+
|
|
68
|
+
type TimeDisplayProps = {
|
|
69
|
+
date: Date,
|
|
70
|
+
mode?: TimeDisplayMode,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A Component for displaying time and dates in a unified fashion
|
|
75
|
+
*/
|
|
76
|
+
export const TimeDisplay = ({
|
|
77
|
+
overwriteTranslation,
|
|
78
|
+
date,
|
|
79
|
+
mode = 'daysFromToday'
|
|
80
|
+
}: PropsForTranslation<TimeDisplayTranslation, TimeDisplayProps>) => {
|
|
81
|
+
const translation = useTranslation(defaultTimeDisplayTranslations, overwriteTranslation)
|
|
82
|
+
const difference = new Date().setHours(0, 0, 0, 0).valueOf() - new Date(date).setHours(0, 0, 0, 0).valueOf()
|
|
83
|
+
const isBefore = difference > 0
|
|
84
|
+
const differenceInDays = Math.floor(Math.abs(difference) / (1000 * 3600 * 24))
|
|
85
|
+
|
|
86
|
+
let displayString
|
|
87
|
+
if (differenceInDays === 0) {
|
|
88
|
+
displayString = translation.today
|
|
89
|
+
} else if (differenceInDays === 1) {
|
|
90
|
+
displayString = isBefore ? translation.yesterday : translation.tomorrow
|
|
91
|
+
} else {
|
|
92
|
+
displayString = isBefore ? translation.agoDays(differenceInDays) : translation.inDays(differenceInDays)
|
|
93
|
+
}
|
|
94
|
+
const monthToTranslation: { [key: number]: string } = {
|
|
95
|
+
0: translation.january,
|
|
96
|
+
1: translation.february,
|
|
97
|
+
2: translation.march,
|
|
98
|
+
3: translation.april,
|
|
99
|
+
4: translation.may,
|
|
100
|
+
5: translation.june,
|
|
101
|
+
6: translation.july,
|
|
102
|
+
7: translation.august,
|
|
103
|
+
8: translation.september,
|
|
104
|
+
9: translation.october,
|
|
105
|
+
10: translation.november,
|
|
106
|
+
11: translation.december
|
|
107
|
+
} as const
|
|
108
|
+
|
|
109
|
+
let fullString
|
|
110
|
+
if (mode === 'daysFromToday') {
|
|
111
|
+
fullString = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')} - ${displayString}`
|
|
112
|
+
} else {
|
|
113
|
+
fullString = `${date.getDate()}. ${monthToTranslation[date.getMonth()]} ${date.getFullYear()}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<span>
|
|
118
|
+
{fullString}
|
|
119
|
+
</span>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CSSProperties, PropsWithChildren, ReactNode } from 'react'
|
|
2
|
+
import { useHoverState } from '../hooks/useHoverState'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
|
|
5
|
+
type Position = 'top' | 'bottom' | 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
export type TooltipProps = PropsWithChildren<{
|
|
8
|
+
tooltip: string | ReactNode,
|
|
9
|
+
/**
|
|
10
|
+
* Number of milliseconds until the tooltip appears
|
|
11
|
+
*
|
|
12
|
+
* defaults to 1000ms
|
|
13
|
+
*/
|
|
14
|
+
animationDelay?: number,
|
|
15
|
+
/**
|
|
16
|
+
* Class names of additional styling properties for the tooltip
|
|
17
|
+
*/
|
|
18
|
+
tooltipClassName?: string,
|
|
19
|
+
/**
|
|
20
|
+
* Class names of additional styling properties for the container from which the tooltip will be created
|
|
21
|
+
*/
|
|
22
|
+
containerClassName?: string,
|
|
23
|
+
position?: Position,
|
|
24
|
+
zIndex?: number,
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A Component for showing a tooltip when hovering over Content
|
|
29
|
+
* @param tooltip The tooltip to show can be a text or any ReactNode
|
|
30
|
+
* @param children The Content for which the tooltip should be created
|
|
31
|
+
* @param animationDelay The delay before the tooltip appears
|
|
32
|
+
* @param tooltipClassName Additional ClassNames for the Container of the tooltip
|
|
33
|
+
* @param containerClassName Additional ClassNames for the Container holding the content
|
|
34
|
+
* @param position The direction of the tooltip relative to the Container
|
|
35
|
+
* @param zIndex The z Index of the tooltip (you may require this when stacking modals)
|
|
36
|
+
* @constructor
|
|
37
|
+
*/
|
|
38
|
+
export const Tooltip = ({
|
|
39
|
+
tooltip,
|
|
40
|
+
children,
|
|
41
|
+
animationDelay = 650,
|
|
42
|
+
tooltipClassName = '',
|
|
43
|
+
containerClassName = '',
|
|
44
|
+
position = 'bottom',
|
|
45
|
+
zIndex = 10,
|
|
46
|
+
}: TooltipProps) => {
|
|
47
|
+
const { isHovered, handlers } = useHoverState()
|
|
48
|
+
|
|
49
|
+
const positionClasses = {
|
|
50
|
+
top: `bottom-full left-1/2 -translate-x-1/2 mb-[6px]`,
|
|
51
|
+
bottom: `top-full left-1/2 -translate-x-1/2 mt-[6px]`,
|
|
52
|
+
left: `right-full top-1/2 -translate-y-1/2 mr-[6px]`,
|
|
53
|
+
right: `left-full top-1/2 -translate-y-1/2 ml-[6px]`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const triangleSize = 6
|
|
57
|
+
const triangleClasses = {
|
|
58
|
+
top: `top-full left-1/2 -translate-x-1/2 border-t-gray-600 border-l-transparent border-r-transparent`,
|
|
59
|
+
bottom: `bottom-full left-1/2 -translate-x-1/2 border-b-gray-600 border-l-transparent border-r-transparent`,
|
|
60
|
+
left: `left-full top-1/2 -translate-y-1/2 border-l-gray-600 border-t-transparent border-b-transparent`,
|
|
61
|
+
right: `right-full top-1/2 -translate-y-1/2 border-r-gray-600 border-t-transparent border-b-transparent`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const triangleStyle: Record<Position, CSSProperties> = {
|
|
65
|
+
top: { borderWidth: `${triangleSize}px ${triangleSize}px 0 ${triangleSize}px` },
|
|
66
|
+
bottom: { borderWidth: `0 ${triangleSize}px ${triangleSize}px ${triangleSize}px` },
|
|
67
|
+
left: { borderWidth: `${triangleSize}px 0 ${triangleSize}px ${triangleSize}px` },
|
|
68
|
+
right: { borderWidth: `${triangleSize}px ${triangleSize}px ${triangleSize}px 0` }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={clsx('relative inline-block', containerClassName)}
|
|
74
|
+
{...handlers}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
{isHovered && (
|
|
78
|
+
<div
|
|
79
|
+
className={clsx(`opacity-0 absolute text-black text-xs font-semibold text-gray-600 px-2 py-1 rounded whitespace-nowrap border-2 border-gray-600
|
|
80
|
+
animate-tooltip-fade-in shadow-lg bg-gray-100`, positionClasses[position], tooltipClassName)}
|
|
81
|
+
style={{ zIndex, animationDelay: animationDelay + 'ms' }}
|
|
82
|
+
>
|
|
83
|
+
{tooltip}
|
|
84
|
+
<div
|
|
85
|
+
className={clsx(`absolute w-0 h-0`, triangleClasses[position])}
|
|
86
|
+
style={{ ...triangleStyle[position], zIndex }}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type VerticalDividerProps = {
|
|
2
|
+
width?: number,
|
|
3
|
+
height?: number,
|
|
4
|
+
strokeWidth?: number,
|
|
5
|
+
dashGap?: number,
|
|
6
|
+
dashLength?: number,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A Component for creating a vertical Divider
|
|
11
|
+
*/
|
|
12
|
+
export const VerticalDivider = ({
|
|
13
|
+
width = 1,
|
|
14
|
+
height = 100,
|
|
15
|
+
strokeWidth = 4,
|
|
16
|
+
dashGap = 4,
|
|
17
|
+
dashLength = 4,
|
|
18
|
+
}: VerticalDividerProps) => {
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ width: width + 'px', height: height + 'px' }}>
|
|
21
|
+
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} fill="none"
|
|
22
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
23
|
+
<line
|
|
24
|
+
opacity="0.5"
|
|
25
|
+
x1={width / 2}
|
|
26
|
+
y1={height}
|
|
27
|
+
x2={width / 2}
|
|
28
|
+
y2="0"
|
|
29
|
+
stroke="url(#paint_linear)"
|
|
30
|
+
strokeWidth={strokeWidth}
|
|
31
|
+
strokeDasharray={`${dashLength} ${dashLength + dashGap}`}
|
|
32
|
+
strokeLinecap="round"
|
|
33
|
+
/>
|
|
34
|
+
<defs>
|
|
35
|
+
<linearGradient
|
|
36
|
+
id="paint_linear"
|
|
37
|
+
x1={width / 2}
|
|
38
|
+
y1="0"
|
|
39
|
+
x2={width / 2}
|
|
40
|
+
y2={height}
|
|
41
|
+
gradientUnits="userSpaceOnUse"
|
|
42
|
+
>
|
|
43
|
+
<stop stopOpacity="0" stopColor="currentColor"/>
|
|
44
|
+
<stop offset="0.5" stopColor="currentColor"/>
|
|
45
|
+
<stop offset="1" stopColor="currentColor" stopOpacity="0"/>
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
</svg>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { ArrowDown, ArrowUp, ChevronDown } from 'lucide-react'
|
|
3
|
+
import type { Languages } from '../../hooks/useLanguage'
|
|
4
|
+
import type { PropsForTranslation } from '../../hooks/useTranslation'
|
|
5
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
6
|
+
import { noop } from '../../util/noop'
|
|
7
|
+
import { addDuration, isInTimeSpan, subtractDuration } from '../../util/date'
|
|
8
|
+
import clsx from 'clsx'
|
|
9
|
+
import { SolidButton, TextButton } from '../Button'
|
|
10
|
+
import { useLocale } from '../../hooks/useLanguage'
|
|
11
|
+
import type { YearMonthPickerProps } from './YearMonthPicker'
|
|
12
|
+
import { YearMonthPicker } from './YearMonthPicker'
|
|
13
|
+
import type { DayPickerProps } from './DayPicker'
|
|
14
|
+
import { DayPicker } from './DayPicker'
|
|
15
|
+
|
|
16
|
+
type DatePickerTranslation = {
|
|
17
|
+
today: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultDatePickerTranslation: Record<Languages, DatePickerTranslation> = {
|
|
21
|
+
en: {
|
|
22
|
+
today: 'Today',
|
|
23
|
+
},
|
|
24
|
+
de: {
|
|
25
|
+
today: 'Heute',
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type DisplayMode = 'yearMonth' | 'day'
|
|
30
|
+
|
|
31
|
+
export type DatePickerProps = {
|
|
32
|
+
value?: Date,
|
|
33
|
+
start?: Date,
|
|
34
|
+
end?: Date,
|
|
35
|
+
initialDisplay?: DisplayMode,
|
|
36
|
+
onChange?: (date: Date) => void,
|
|
37
|
+
dayPickerProps?: Omit<DayPickerProps, 'displayedMonth' | 'onChange' | 'selected'>,
|
|
38
|
+
yearMonthPickerProps?: Omit<YearMonthPickerProps, 'displayedYearMonth' | 'onChange' | 'start' | 'end'>,
|
|
39
|
+
className?: string,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A Component for picking a date
|
|
44
|
+
*/
|
|
45
|
+
export const DatePicker = ({
|
|
46
|
+
overwriteTranslation,
|
|
47
|
+
value = new Date(),
|
|
48
|
+
start = subtractDuration(new Date(), { years: 50 }),
|
|
49
|
+
end = addDuration(new Date(), { years: 50 }),
|
|
50
|
+
initialDisplay = 'day',
|
|
51
|
+
onChange = noop,
|
|
52
|
+
yearMonthPickerProps,
|
|
53
|
+
dayPickerProps,
|
|
54
|
+
className = ''
|
|
55
|
+
}: PropsForTranslation<DatePickerTranslation, DatePickerProps>) => {
|
|
56
|
+
const locale = useLocale()
|
|
57
|
+
const translation = useTranslation(defaultDatePickerTranslation, overwriteTranslation)
|
|
58
|
+
const [displayedMonth, setDisplayedMonth] = useState<Date>(value)
|
|
59
|
+
const [displayMode, setDisplayMode] = useState<DisplayMode>(initialDisplay)
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setDisplayedMonth(value)
|
|
63
|
+
}, [value])
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={clsx('col gap-y-4', className)}>
|
|
67
|
+
<div className="row items-center justify-between h-7">
|
|
68
|
+
<TextButton
|
|
69
|
+
className={clsx('row gap-x-1 items-center cursor-pointer select-none', {
|
|
70
|
+
'text-disabled-text': displayMode !== 'day',
|
|
71
|
+
})}
|
|
72
|
+
onClick={() => setDisplayMode(displayMode === 'day' ? 'yearMonth' : 'day')}
|
|
73
|
+
>
|
|
74
|
+
{`${new Intl.DateTimeFormat(locale, { month: 'long' }).format(displayedMonth)} ${displayedMonth.getFullYear()}`}
|
|
75
|
+
<ChevronDown size={16}/>
|
|
76
|
+
</TextButton>
|
|
77
|
+
{displayMode === 'day' && (
|
|
78
|
+
<div className="row justify-end">
|
|
79
|
+
<SolidButton
|
|
80
|
+
size="small"
|
|
81
|
+
color="primary"
|
|
82
|
+
disabled={!isInTimeSpan(subtractDuration(displayedMonth, { months: 1 }), start, end)}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
setDisplayedMonth(subtractDuration(displayedMonth, { months: 1 }))
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<ArrowUp size={20}/>
|
|
88
|
+
</SolidButton>
|
|
89
|
+
<SolidButton
|
|
90
|
+
size="small"
|
|
91
|
+
color="primary"
|
|
92
|
+
disabled={!isInTimeSpan(addDuration(displayedMonth, { months: 1 }), start, end)}
|
|
93
|
+
onClick={() => {
|
|
94
|
+
setDisplayedMonth(addDuration(displayedMonth, { months: 1 }))
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<ArrowDown size={20}/>
|
|
98
|
+
</SolidButton>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{displayMode === 'yearMonth' ? (
|
|
103
|
+
<YearMonthPicker
|
|
104
|
+
{...yearMonthPickerProps}
|
|
105
|
+
displayedYearMonth={value}
|
|
106
|
+
start={start}
|
|
107
|
+
end={end}
|
|
108
|
+
onChange={newDate => {
|
|
109
|
+
setDisplayedMonth(newDate)
|
|
110
|
+
setDisplayMode('day')
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
) : (
|
|
114
|
+
<div>
|
|
115
|
+
<DayPicker
|
|
116
|
+
{...dayPickerProps}
|
|
117
|
+
displayedMonth={displayedMonth}
|
|
118
|
+
start={start}
|
|
119
|
+
end={end}
|
|
120
|
+
selected={value}
|
|
121
|
+
onChange={date => {
|
|
122
|
+
onChange(date)
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
<div className="mt-2">
|
|
126
|
+
<TextButton
|
|
127
|
+
onClick={() => {
|
|
128
|
+
const newDate = new Date()
|
|
129
|
+
newDate.setHours(value.getHours(), value.getMinutes())
|
|
130
|
+
onChange(newDate)
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{translation.today}
|
|
134
|
+
</TextButton>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Example for the Date Picker
|
|
144
|
+
*/
|
|
145
|
+
export const ControlledDatePicker = ({
|
|
146
|
+
value = new Date(),
|
|
147
|
+
onChange = noop,
|
|
148
|
+
...props
|
|
149
|
+
}: DatePickerProps) => {
|
|
150
|
+
const [date, setDate] = useState<Date>(value)
|
|
151
|
+
|
|
152
|
+
useEffect(() => setDate(value), [value])
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<DatePicker
|
|
156
|
+
{...props}
|
|
157
|
+
value={date}
|
|
158
|
+
onChange={date1 => {
|
|
159
|
+
setDate(date1)
|
|
160
|
+
onChange(date1)
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { WeekDay } from '../../util/date'
|
|
2
|
+
import { isInTimeSpan } from '../../util/date'
|
|
3
|
+
import { equalDate, getWeeksForCalenderMonth } from '../../util/date'
|
|
4
|
+
import { noop } from '../../util/noop'
|
|
5
|
+
import clsx from 'clsx'
|
|
6
|
+
import { useLocale } from '../../hooks/useLanguage'
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
export type DayPickerProps = {
|
|
10
|
+
displayedMonth: Date,
|
|
11
|
+
selected?: Date,
|
|
12
|
+
start?: Date,
|
|
13
|
+
end?: Date,
|
|
14
|
+
onChange?: (date: Date) => void,
|
|
15
|
+
weekStart?: WeekDay,
|
|
16
|
+
markToday?: boolean,
|
|
17
|
+
className?: string,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A component for selecting a day of a month
|
|
22
|
+
*/
|
|
23
|
+
export const DayPicker = ({
|
|
24
|
+
displayedMonth,
|
|
25
|
+
selected,
|
|
26
|
+
start,
|
|
27
|
+
end,
|
|
28
|
+
onChange = noop,
|
|
29
|
+
weekStart = 'monday',
|
|
30
|
+
markToday = true,
|
|
31
|
+
className = ''
|
|
32
|
+
}: DayPickerProps) => {
|
|
33
|
+
const locale = useLocale()
|
|
34
|
+
const month = displayedMonth.getMonth()
|
|
35
|
+
const weeks = getWeeksForCalenderMonth(displayedMonth, weekStart)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={clsx('col gap-y-1 min-w-[220px] select-none', className)}>
|
|
39
|
+
<div className="row text-center">
|
|
40
|
+
{weeks[0]!.map((weekDay, index) => (
|
|
41
|
+
<div key={index} className="flex-1 font-semibold">
|
|
42
|
+
{new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(weekDay).substring(0, 2)}
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
{weeks.map((week, index) => (
|
|
47
|
+
<div key={index} className="row text-center">
|
|
48
|
+
{week.map((date) => {
|
|
49
|
+
const isSelected = !!selected && equalDate(selected, date)
|
|
50
|
+
const isToday = equalDate(new Date(), date)
|
|
51
|
+
const isSameMonth = date.getMonth() === month
|
|
52
|
+
const isDayValid = isInTimeSpan(date, start, end)
|
|
53
|
+
return (
|
|
54
|
+
<button
|
|
55
|
+
disabled={!isDayValid}
|
|
56
|
+
key={date.getDate()}
|
|
57
|
+
className={clsx(
|
|
58
|
+
'flex-1 rounded-full border-2 border-transparent shadow-sm',
|
|
59
|
+
{
|
|
60
|
+
'text-gray-700 bg-gray-100': !isSameMonth && isDayValid,
|
|
61
|
+
'text-black bg-white': !isSelected && isSameMonth && isDayValid,
|
|
62
|
+
'text-on-primary bg-primary': isSelected,
|
|
63
|
+
'border-black': isToday && markToday,
|
|
64
|
+
'hover:brightness-90 hover:bg-primary hover:text-on-primary': isDayValid,
|
|
65
|
+
'text-disabled-text bg-disabled-background': !isDayValid
|
|
66
|
+
}
|
|
67
|
+
)}
|
|
68
|
+
onClick={() => onChange(date)}
|
|
69
|
+
>
|
|
70
|
+
{date.getDate()}
|
|
71
|
+
</button>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const DayPickerControlled = ({ displayedMonth, onChange = noop, ...restProps }: DayPickerProps) => {
|
|
81
|
+
const [date, setDate] = useState(displayedMonth)
|
|
82
|
+
|
|
83
|
+
useEffect(() => setDate(displayedMonth), [displayedMonth])
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<DayPicker
|
|
87
|
+
displayedMonth={date}
|
|
88
|
+
onChange={newDate => {
|
|
89
|
+
setDate(newDate)
|
|
90
|
+
onChange(newDate)
|
|
91
|
+
}}
|
|
92
|
+
{...restProps}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Scrollbars } from 'react-custom-scrollbars-2'
|
|
3
|
+
import { noop } from '../../util/noop'
|
|
4
|
+
import { closestMatch, range } from '../../util/array'
|
|
5
|
+
import clsx from 'clsx'
|
|
6
|
+
|
|
7
|
+
type MinuteIncrement = '1min' | '5min' | '10min' | '15min' | '30min'
|
|
8
|
+
|
|
9
|
+
export type TimePickerProps = {
|
|
10
|
+
time?: Date,
|
|
11
|
+
onChange?: (time: Date) => void,
|
|
12
|
+
is24HourFormat?: boolean,
|
|
13
|
+
minuteIncrement?: MinuteIncrement,
|
|
14
|
+
maxHeight?: number,
|
|
15
|
+
className?: string,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const TimePicker = ({
|
|
19
|
+
time = new Date(),
|
|
20
|
+
onChange = noop,
|
|
21
|
+
is24HourFormat = true,
|
|
22
|
+
minuteIncrement = '5min',
|
|
23
|
+
maxHeight = 300,
|
|
24
|
+
className = ''
|
|
25
|
+
}: TimePickerProps) => {
|
|
26
|
+
const minuteRef = useRef<HTMLButtonElement>(null)
|
|
27
|
+
const hourRef = useRef<HTMLButtonElement>(null)
|
|
28
|
+
|
|
29
|
+
const isPM = time.getHours() >= 11
|
|
30
|
+
const hours = is24HourFormat ? range(0, 23) : range(1, 12)
|
|
31
|
+
let minutes = range(0, 59)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const scrollToItem = () => {
|
|
35
|
+
if (minuteRef.current) {
|
|
36
|
+
const container = minuteRef.current.parentElement!
|
|
37
|
+
|
|
38
|
+
const hasOverflow = container.scrollHeight > maxHeight
|
|
39
|
+
if (hasOverflow) {
|
|
40
|
+
minuteRef.current.scrollIntoView({
|
|
41
|
+
behavior: 'instant',
|
|
42
|
+
block: 'nearest',
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
scrollToItem()
|
|
48
|
+
}, [minuteRef, minuteRef.current]) // eslint-disable-line
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const scrollToItem = () => {
|
|
52
|
+
if (hourRef.current) {
|
|
53
|
+
const container = hourRef.current.parentElement!
|
|
54
|
+
|
|
55
|
+
const hasOverflow = container.scrollHeight > maxHeight
|
|
56
|
+
if (hasOverflow) {
|
|
57
|
+
hourRef.current.scrollIntoView({
|
|
58
|
+
behavior: 'instant',
|
|
59
|
+
block: 'nearest',
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
scrollToItem()
|
|
65
|
+
}, [hourRef, hourRef.current]) // eslint-disable-line
|
|
66
|
+
|
|
67
|
+
switch (minuteIncrement) {
|
|
68
|
+
case '5min':
|
|
69
|
+
minutes = minutes.filter(value => value % 5 === 0)
|
|
70
|
+
break
|
|
71
|
+
case '10min':
|
|
72
|
+
minutes = minutes.filter(value => value % 10 === 0)
|
|
73
|
+
break
|
|
74
|
+
case '15min':
|
|
75
|
+
minutes = minutes.filter(value => value % 15 === 0)
|
|
76
|
+
break
|
|
77
|
+
case '30min':
|
|
78
|
+
minutes = minutes.filter(value => value % 30 === 0)
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const closestMinute = closestMatch(minutes, (item1, item2) => Math.abs(item1 - time.getMinutes()) < Math.abs(item2 - time.getMinutes()))
|
|
83
|
+
|
|
84
|
+
const style = (selected: boolean) => clsx('chip-full hover:brightness-90 hover:bg-primary hover:text-on-primary rounded-md mr-3',
|
|
85
|
+
{ 'bg-primary text-on-primary': selected, 'bg-white text-black': !selected })
|
|
86
|
+
|
|
87
|
+
const onChangeWrapper = (transformer: (newDate: Date) => void) => {
|
|
88
|
+
const newDate = new Date(time)
|
|
89
|
+
transformer(newDate)
|
|
90
|
+
onChange(newDate)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={clsx('row gap-x-2 w-fit min-w-[150px] select-none', className)}>
|
|
95
|
+
<Scrollbars autoHeight autoHeightMax={maxHeight} style={{ height: '100%' }}>
|
|
96
|
+
<div className="col gap-y-1 h-full">
|
|
97
|
+
{hours.map(hour => {
|
|
98
|
+
const currentHour = hour === time.getHours() - (!is24HourFormat && isPM ? 12 : 0)
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
key={hour}
|
|
102
|
+
ref={currentHour ? hourRef : undefined}
|
|
103
|
+
className={style(currentHour)}
|
|
104
|
+
onClick={() => onChangeWrapper(newDate => newDate.setHours(hour + (!is24HourFormat && isPM ? 12 : 0)))}
|
|
105
|
+
>
|
|
106
|
+
{hour.toString().padStart(2, '0')}
|
|
107
|
+
</button>
|
|
108
|
+
)
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
</Scrollbars>
|
|
112
|
+
<Scrollbars autoHeight autoHeightMax={maxHeight} style={{ height: '100%' }}>
|
|
113
|
+
<div className="col gap-y-1 h-full">
|
|
114
|
+
{minutes.map(minute => {
|
|
115
|
+
const currentMinute = minute === closestMinute
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
key={minute + minuteIncrement} // minute increment so that scroll works
|
|
119
|
+
ref={currentMinute ? minuteRef : undefined}
|
|
120
|
+
className={style(currentMinute)}
|
|
121
|
+
onClick={() => onChangeWrapper(newDate => newDate.setMinutes(minute))}
|
|
122
|
+
>
|
|
123
|
+
{minute.toString().padStart(2, '0')}
|
|
124
|
+
</button>
|
|
125
|
+
)
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
</Scrollbars>
|
|
129
|
+
{!is24HourFormat && (
|
|
130
|
+
<div className="col gap-y-1">
|
|
131
|
+
<button
|
|
132
|
+
className={style(!isPM)}
|
|
133
|
+
onClick={() => onChangeWrapper(newDate => isPM && newDate.setHours(newDate.getHours() - 12))}
|
|
134
|
+
>
|
|
135
|
+
AM
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
className={style(isPM)}
|
|
139
|
+
onClick={() => onChangeWrapper(newDate => !isPM && newDate.setHours(newDate.getHours() + 12))}
|
|
140
|
+
>
|
|
141
|
+
PM
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const ControlledTimePicker = ({
|
|
150
|
+
time,
|
|
151
|
+
onChange = noop,
|
|
152
|
+
...props
|
|
153
|
+
}: TimePickerProps) => {
|
|
154
|
+
const [value, setValue] = useState(time)
|
|
155
|
+
useEffect(() => setValue(time), [time])
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<TimePicker
|
|
159
|
+
{...props}
|
|
160
|
+
time={value}
|
|
161
|
+
onChange={time1 => {
|
|
162
|
+
setValue(time1)
|
|
163
|
+
onChange(time1)
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
167
|
+
}
|