@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,156 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
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, subtractDuration } from '../../util/date'
|
|
8
|
+
import { SolidButton } from '../Button'
|
|
9
|
+
import type { TimePickerProps } from '../date/TimePicker'
|
|
10
|
+
import { TimePicker } from '../date/TimePicker'
|
|
11
|
+
import type { DatePickerProps } from '../date/DatePicker'
|
|
12
|
+
import { DatePicker } from '../date/DatePicker'
|
|
13
|
+
|
|
14
|
+
type TimeTranslation = {
|
|
15
|
+
clear: string,
|
|
16
|
+
change: string,
|
|
17
|
+
year: string,
|
|
18
|
+
month: string,
|
|
19
|
+
day: string,
|
|
20
|
+
january: string,
|
|
21
|
+
february: string,
|
|
22
|
+
march: string,
|
|
23
|
+
april: string,
|
|
24
|
+
may: string,
|
|
25
|
+
june: string,
|
|
26
|
+
july: string,
|
|
27
|
+
august: string,
|
|
28
|
+
september: string,
|
|
29
|
+
october: string,
|
|
30
|
+
november: string,
|
|
31
|
+
december: string,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultTimeTranslation: Record<Languages, TimeTranslation> = {
|
|
35
|
+
en: {
|
|
36
|
+
clear: 'Clear',
|
|
37
|
+
change: 'Change',
|
|
38
|
+
year: 'Year',
|
|
39
|
+
month: 'Month',
|
|
40
|
+
day: 'Day',
|
|
41
|
+
january: 'January',
|
|
42
|
+
february: 'Febuary',
|
|
43
|
+
march: 'March',
|
|
44
|
+
april: 'April',
|
|
45
|
+
may: 'May',
|
|
46
|
+
june: 'June',
|
|
47
|
+
july: 'July',
|
|
48
|
+
august: 'August',
|
|
49
|
+
september: 'September',
|
|
50
|
+
october: 'October',
|
|
51
|
+
november: 'November',
|
|
52
|
+
december: 'December',
|
|
53
|
+
},
|
|
54
|
+
de: {
|
|
55
|
+
clear: 'Entfernen',
|
|
56
|
+
change: 'Ändern',
|
|
57
|
+
year: 'Jahr',
|
|
58
|
+
month: 'Monat',
|
|
59
|
+
day: 'Tag',
|
|
60
|
+
january: 'Januar',
|
|
61
|
+
february: 'Febuar',
|
|
62
|
+
march: 'März',
|
|
63
|
+
april: 'April',
|
|
64
|
+
may: 'Mai',
|
|
65
|
+
june: 'Juni',
|
|
66
|
+
july: 'Juli',
|
|
67
|
+
august: 'August',
|
|
68
|
+
september: 'September',
|
|
69
|
+
october: 'October',
|
|
70
|
+
november: 'November',
|
|
71
|
+
december: 'December',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type DateTimePickerMode = 'date' | 'time' | 'dateTime'
|
|
76
|
+
|
|
77
|
+
export type DateTimePickerProps = {
|
|
78
|
+
mode?: DateTimePickerMode,
|
|
79
|
+
value?: Date,
|
|
80
|
+
start?: Date,
|
|
81
|
+
end?: Date,
|
|
82
|
+
onChange?: (date: Date) => void,
|
|
83
|
+
onFinish?: (date: Date) => void,
|
|
84
|
+
onRemove?: () => void,
|
|
85
|
+
datePickerProps?: Omit<DatePickerProps, 'onChange' | 'value' | 'start' | 'end'>,
|
|
86
|
+
timePickerProps?: Omit<TimePickerProps, 'onChange' | 'time' | 'maxHeight'>,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A Component for picking a Date and Time
|
|
91
|
+
*/
|
|
92
|
+
export const DateTimePicker = ({
|
|
93
|
+
overwriteTranslation,
|
|
94
|
+
value = new Date(),
|
|
95
|
+
start = subtractDuration(new Date(), { years: 50 }),
|
|
96
|
+
end = addDuration(new Date(), { years: 50 }),
|
|
97
|
+
mode = 'dateTime',
|
|
98
|
+
onFinish = noop,
|
|
99
|
+
onChange = noop,
|
|
100
|
+
onRemove = noop,
|
|
101
|
+
timePickerProps,
|
|
102
|
+
datePickerProps,
|
|
103
|
+
}: PropsForTranslation<TimeTranslation, DateTimePickerProps>) => {
|
|
104
|
+
const translation = useTranslation(defaultTimeTranslation, overwriteTranslation)
|
|
105
|
+
|
|
106
|
+
const useDate = mode === 'dateTime' || mode === 'date'
|
|
107
|
+
const useTime = mode === 'dateTime' || mode === 'time'
|
|
108
|
+
|
|
109
|
+
let dateDisplay: ReactNode
|
|
110
|
+
let timeDisplay: ReactNode
|
|
111
|
+
|
|
112
|
+
if (useDate) {
|
|
113
|
+
dateDisplay = (
|
|
114
|
+
<DatePicker
|
|
115
|
+
{...datePickerProps}
|
|
116
|
+
className="min-w-[320px] min-h-[250px]"
|
|
117
|
+
yearMonthPickerProps={{ maxHeight: 218 }}
|
|
118
|
+
value={value}
|
|
119
|
+
start={start}
|
|
120
|
+
end={end}
|
|
121
|
+
onChange={onChange}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
if (useTime) {
|
|
126
|
+
timeDisplay = (
|
|
127
|
+
<TimePicker
|
|
128
|
+
{...timePickerProps}
|
|
129
|
+
className={clsx('h-full', { 'justify-between w-full': mode === 'time' })}
|
|
130
|
+
maxHeight={250}
|
|
131
|
+
time={value}
|
|
132
|
+
onChange={onChange}
|
|
133
|
+
/>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="col w-fit">
|
|
139
|
+
<div className="row gap-x-4">
|
|
140
|
+
{dateDisplay}
|
|
141
|
+
{timeDisplay}
|
|
142
|
+
</div>
|
|
143
|
+
<div className="row justify-end">
|
|
144
|
+
<div className="row gap-x-2 mt-1">
|
|
145
|
+
<SolidButton size="medium" color="negative" onClick={onRemove}>{translation.clear}</SolidButton>
|
|
146
|
+
<SolidButton
|
|
147
|
+
size="medium"
|
|
148
|
+
onClick={() => onFinish(value)}
|
|
149
|
+
>
|
|
150
|
+
{translation.change}
|
|
151
|
+
</SolidButton>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
type ChangeEvent,
|
|
6
|
+
type HTMLInputTypeAttribute,
|
|
7
|
+
type InputHTMLAttributes, forwardRef
|
|
8
|
+
} from 'react'
|
|
9
|
+
import clsx from 'clsx'
|
|
10
|
+
import useSaveDelay from '../../hooks/useSaveDelay'
|
|
11
|
+
import { noop } from '../../util/noop'
|
|
12
|
+
import type { LabelProps } from './Label'
|
|
13
|
+
import { Label } from './Label'
|
|
14
|
+
|
|
15
|
+
export type InputProps = {
|
|
16
|
+
/**
|
|
17
|
+
* used for the label's `for` attribute
|
|
18
|
+
*/
|
|
19
|
+
id?: string,
|
|
20
|
+
value: string,
|
|
21
|
+
label?: Omit<LabelProps, 'id'>,
|
|
22
|
+
/**
|
|
23
|
+
* @default 'text'
|
|
24
|
+
*/
|
|
25
|
+
type?: HTMLInputTypeAttribute,
|
|
26
|
+
/**
|
|
27
|
+
* Callback for when the input's value changes
|
|
28
|
+
* This is pretty much required but made optional for the rare cases where it actually isn't need such as when used with disabled
|
|
29
|
+
* That could be enforced through a union type but that seems a bit overkill
|
|
30
|
+
* @default noop
|
|
31
|
+
*/
|
|
32
|
+
onChange?: (text: string, event: ChangeEvent<HTMLInputElement>) => void,
|
|
33
|
+
onChangeEvent?: (event: ChangeEvent<HTMLInputElement>) => void,
|
|
34
|
+
className?: string,
|
|
35
|
+
onEditCompleted?: (text: string, event: ChangeEvent<HTMLInputElement>) => void,
|
|
36
|
+
expanded?: boolean,
|
|
37
|
+
containerClassName?: string,
|
|
38
|
+
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'value' | 'label' | 'type' | 'onChange' | 'crossOrigin'>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A Component for inputting text or other information
|
|
42
|
+
*
|
|
43
|
+
* Its state is managed must be managed by the parent
|
|
44
|
+
*/
|
|
45
|
+
const ControlledInput = ({
|
|
46
|
+
id,
|
|
47
|
+
type = 'text',
|
|
48
|
+
value,
|
|
49
|
+
label,
|
|
50
|
+
onChange = noop,
|
|
51
|
+
onChangeEvent = noop,
|
|
52
|
+
className = '',
|
|
53
|
+
onEditCompleted,
|
|
54
|
+
expanded = true,
|
|
55
|
+
onBlur,
|
|
56
|
+
containerClassName,
|
|
57
|
+
...restProps
|
|
58
|
+
}: InputProps) => {
|
|
59
|
+
const {
|
|
60
|
+
restartTimer,
|
|
61
|
+
clearUpdateTimer
|
|
62
|
+
} = useSaveDelay(() => undefined, 3000)
|
|
63
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (restProps.autoFocus) {
|
|
67
|
+
ref.current?.focus()
|
|
68
|
+
}
|
|
69
|
+
}, [restProps.autoFocus])
|
|
70
|
+
return (
|
|
71
|
+
<div className={clsx({ 'w-full': expanded }, containerClassName)}>
|
|
72
|
+
{label && <Label {...label} htmlFor={id} className={clsx('mb-1', label.className)}/>}
|
|
73
|
+
<input
|
|
74
|
+
ref={ref}
|
|
75
|
+
value={value}
|
|
76
|
+
id={id}
|
|
77
|
+
type={type}
|
|
78
|
+
className={clsx('block bg-surface text-on-surface px-3 py-2 rounded-md w-full border-2 border-gray-200 hover:border-primary focus:outline-none focus:border-primary focus:ring-primary', className)}
|
|
79
|
+
onBlur={event => {
|
|
80
|
+
if (onBlur) {
|
|
81
|
+
onBlur(event)
|
|
82
|
+
}
|
|
83
|
+
if (onEditCompleted) {
|
|
84
|
+
onEditCompleted(event.target.value, event)
|
|
85
|
+
clearUpdateTimer()
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
onChange={e => {
|
|
89
|
+
const value = e.target.value
|
|
90
|
+
if (onEditCompleted) {
|
|
91
|
+
restartTimer(() => {
|
|
92
|
+
onEditCompleted(value, e)
|
|
93
|
+
clearUpdateTimer()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
onChange(value, e)
|
|
97
|
+
onChangeEvent(e)
|
|
98
|
+
}}
|
|
99
|
+
{...restProps}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type UncontrolledInputProps = Omit<InputProps, 'value'> & {
|
|
106
|
+
/**
|
|
107
|
+
* @default ''
|
|
108
|
+
*/
|
|
109
|
+
defaultValue?: string,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A Component for inputting text or other information
|
|
114
|
+
*
|
|
115
|
+
* Its state is managed by the component itself
|
|
116
|
+
*/
|
|
117
|
+
const UncontrolledInput = ({
|
|
118
|
+
defaultValue = '',
|
|
119
|
+
onChange = noop,
|
|
120
|
+
...props
|
|
121
|
+
}: UncontrolledInputProps) => {
|
|
122
|
+
const [value, setValue] = useState(defaultValue)
|
|
123
|
+
|
|
124
|
+
const handleChange = (text: string, event: ChangeEvent<HTMLInputElement>) => {
|
|
125
|
+
setValue(text)
|
|
126
|
+
onChange(text, event)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ControlledInput
|
|
131
|
+
{...props}
|
|
132
|
+
value={value}
|
|
133
|
+
onChange={handleChange}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type FormInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
139
|
+
id: string,
|
|
140
|
+
labelText?: string,
|
|
141
|
+
errorText?: string,
|
|
142
|
+
labelClassName?: string,
|
|
143
|
+
errorClassName?: string,
|
|
144
|
+
containerClassName?: string,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const FormInput = forwardRef<HTMLInputElement, FormInputProps>(function FormInput({
|
|
148
|
+
id,
|
|
149
|
+
labelText,
|
|
150
|
+
errorText,
|
|
151
|
+
className,
|
|
152
|
+
labelClassName,
|
|
153
|
+
errorClassName,
|
|
154
|
+
containerClassName,
|
|
155
|
+
required,
|
|
156
|
+
...restProps
|
|
157
|
+
}, ref) {
|
|
158
|
+
const input = (
|
|
159
|
+
<input
|
|
160
|
+
ref={ref}
|
|
161
|
+
id={id}
|
|
162
|
+
{...restProps}
|
|
163
|
+
className={clsx(
|
|
164
|
+
'block bg-surface text-on-surface px-3 py-2 rounded-md w-full border-2 border-gray-200 hover:border-primary focus:outline-none focus:border-primary focus:ring-primary',
|
|
165
|
+
{
|
|
166
|
+
'focus:border-primary focus:ring-primary': !errorText,
|
|
167
|
+
'focus:border-negative focus:ring-negative text-negative': !!errorText,
|
|
168
|
+
},
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className={clsx('flex flex-col gap-y-1', containerClassName)}>
|
|
176
|
+
{labelText && (
|
|
177
|
+
<label htmlFor={id} className={clsx('textstyle-label-md', labelClassName)}>
|
|
178
|
+
{labelText}
|
|
179
|
+
{required && <span className="text-primary font-bold">*</span>}
|
|
180
|
+
</label>
|
|
181
|
+
)}
|
|
182
|
+
{input}
|
|
183
|
+
{errorText && <label htmlFor={id} className={clsx('text-negative', errorClassName)}>{errorText}</label>}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
UncontrolledInput,
|
|
190
|
+
ControlledInput as Input,
|
|
191
|
+
FormInput
|
|
192
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LabelHTMLAttributes } from 'react'
|
|
2
|
+
|
|
3
|
+
export type LabelType = 'labelSmall' | 'labelMedium' | 'labelBig'
|
|
4
|
+
const styleMapping: Record<LabelType, string> = {
|
|
5
|
+
labelSmall: 'textstyle-label-sm',
|
|
6
|
+
labelMedium: 'textstyle-label-md',
|
|
7
|
+
labelBig: 'textstyle-label-lg',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export type LabelProps = {
|
|
12
|
+
/** The text for the label */
|
|
13
|
+
name?: string,
|
|
14
|
+
/** The styling for the label */
|
|
15
|
+
labelType?: LabelType,
|
|
16
|
+
} & LabelHTMLAttributes<HTMLLabelElement>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A Label component
|
|
20
|
+
*/
|
|
21
|
+
export const Label = ({
|
|
22
|
+
children,
|
|
23
|
+
name,
|
|
24
|
+
labelType = 'labelSmall',
|
|
25
|
+
...props
|
|
26
|
+
}: LabelProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<label {...props}>
|
|
29
|
+
{children ? children : (<span className={styleMapping[labelType]}>{name}</span>)}
|
|
30
|
+
</label>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useRef, type PropsWithChildren, type ReactNode, type RefObject } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import { useOutsideClick } from '../../hooks/useOutsideClick'
|
|
4
|
+
import { useHoverState } from '../../hooks/useHoverState'
|
|
5
|
+
|
|
6
|
+
type MenuProps<T> = PropsWithChildren<{
|
|
7
|
+
trigger: (onClick: () => void, ref: RefObject<T>) => ReactNode,
|
|
8
|
+
/**
|
|
9
|
+
* @default 'tl'
|
|
10
|
+
*/
|
|
11
|
+
alignment?: 'tl' | 'tr' | 'bl' | 'br' | '_l' | '_r' | 't_' | 'b_',
|
|
12
|
+
showOnHover?: boolean,
|
|
13
|
+
menuClassName?: string,
|
|
14
|
+
}>
|
|
15
|
+
|
|
16
|
+
export type MenuItemProps = {
|
|
17
|
+
onClick?: () => void,
|
|
18
|
+
alignment?: 'left' | 'right',
|
|
19
|
+
className?: string,
|
|
20
|
+
}
|
|
21
|
+
const MenuItem = ({
|
|
22
|
+
children,
|
|
23
|
+
onClick,
|
|
24
|
+
alignment = 'left',
|
|
25
|
+
className
|
|
26
|
+
}: PropsWithChildren<MenuItemProps>) => (
|
|
27
|
+
<div
|
|
28
|
+
className={clsx('block px-3 py-1 bg-menu-background text-menu-text hover:brightness-90', {
|
|
29
|
+
'text-right': alignment === 'right',
|
|
30
|
+
'text-left': alignment === 'left',
|
|
31
|
+
}, className)}
|
|
32
|
+
onClick={onClick}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// TODO: it is quite annoying that the type for the ref has to be specified manually, is there some solution around this?
|
|
39
|
+
/**
|
|
40
|
+
* A Menu Component to allow the user to see different functions
|
|
41
|
+
*/
|
|
42
|
+
const Menu = <T extends HTMLElement>({
|
|
43
|
+
trigger,
|
|
44
|
+
children,
|
|
45
|
+
alignment = 'tl',
|
|
46
|
+
showOnHover = false,
|
|
47
|
+
menuClassName = '',
|
|
48
|
+
}: MenuProps<T>) => {
|
|
49
|
+
const { isHovered: isOpen, setIsHovered: setIsOpen, handlers } = useHoverState({ isDisabled: !showOnHover })
|
|
50
|
+
const triggerRef = useRef<T>(null)
|
|
51
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
useOutsideClick([triggerRef, menuRef], () => setIsOpen(false))
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className="relative"
|
|
57
|
+
{...handlers}
|
|
58
|
+
>
|
|
59
|
+
{trigger(() => setIsOpen(!isOpen), triggerRef)}
|
|
60
|
+
{isOpen ? (
|
|
61
|
+
<div ref={menuRef} onClick={e => e.stopPropagation()}
|
|
62
|
+
className={clsx('absolute top-full mt-1 py-2 w-60 rounded-lg bg-menu-background text-menu-text ring-1 ring-slate-900/5 text-sm leading-6 font-semibold shadow-md z-[1]', {
|
|
63
|
+
' top-[8px]': alignment[0] === 't',
|
|
64
|
+
' bottom-[8px]': alignment[0] === 'b',
|
|
65
|
+
' left-[-8px]': alignment[1] === 'l',
|
|
66
|
+
' right-[-8px]': alignment[1] === 'r',
|
|
67
|
+
}, menuClassName)}>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { Menu, MenuItem }
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { Search } from 'lucide-react'
|
|
4
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
5
|
+
import type { PropsForTranslation } from '../../hooks/useTranslation'
|
|
6
|
+
import type { Languages } from '../../hooks/useLanguage'
|
|
7
|
+
import { MultiSearchWithMapping } from '../../util/simpleSearch'
|
|
8
|
+
import clsx from 'clsx'
|
|
9
|
+
import { Menu, MenuItem } from './Menu'
|
|
10
|
+
import { Input } from './Input'
|
|
11
|
+
import { Checkbox } from './Checkbox'
|
|
12
|
+
import type { LabelProps } from './Label'
|
|
13
|
+
import { Label } from './Label'
|
|
14
|
+
|
|
15
|
+
type MultiSelectTranslation = {
|
|
16
|
+
select: string,
|
|
17
|
+
search: string,
|
|
18
|
+
selected: string,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultMultiSelectTranslation: Record<Languages, MultiSelectTranslation> = {
|
|
22
|
+
en: {
|
|
23
|
+
select: 'Select',
|
|
24
|
+
search: 'Search',
|
|
25
|
+
selected: 'selected'
|
|
26
|
+
},
|
|
27
|
+
de: {
|
|
28
|
+
select: 'Auswählen',
|
|
29
|
+
search: 'Suche',
|
|
30
|
+
selected: 'ausgewählt'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// TODO maybe add custom item builder here
|
|
35
|
+
export type MultiSelectOption<T> = {
|
|
36
|
+
label: string,
|
|
37
|
+
value: T,
|
|
38
|
+
selected: boolean,
|
|
39
|
+
disabled?: boolean,
|
|
40
|
+
className?: string,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type SearchProps<T> = {
|
|
44
|
+
initialSearch?: string,
|
|
45
|
+
searchMapping: (value: MultiSelectOption<T>) => string[],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type MultiSelectProps<T> = {
|
|
49
|
+
options: MultiSelectOption<T>[],
|
|
50
|
+
onChange: (options: MultiSelectOption<T>[]) => void,
|
|
51
|
+
search?: SearchProps<T>,
|
|
52
|
+
disabled?: boolean,
|
|
53
|
+
selectedDisplay?: (props: {
|
|
54
|
+
items: MultiSelectOption<T>[],
|
|
55
|
+
disabled: boolean,
|
|
56
|
+
}) => ReactNode,
|
|
57
|
+
label?: LabelProps,
|
|
58
|
+
hintText?: string,
|
|
59
|
+
showDisabledOptions?: boolean,
|
|
60
|
+
className?: string,
|
|
61
|
+
triggerClassName?: string,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A Component for multi selection
|
|
66
|
+
*/
|
|
67
|
+
export const MultiSelect = <T, >({
|
|
68
|
+
overwriteTranslation,
|
|
69
|
+
options,
|
|
70
|
+
onChange,
|
|
71
|
+
search,
|
|
72
|
+
disabled = false,
|
|
73
|
+
selectedDisplay,
|
|
74
|
+
label,
|
|
75
|
+
hintText,
|
|
76
|
+
showDisabledOptions = true,
|
|
77
|
+
className = '',
|
|
78
|
+
triggerClassName = '',
|
|
79
|
+
}: PropsForTranslation<MultiSelectTranslation, MultiSelectProps<T>>) => {
|
|
80
|
+
const translation = useTranslation(defaultMultiSelectTranslation, overwriteTranslation)
|
|
81
|
+
const [searchText, setSearchText] = useState<string>(search?.initialSearch ?? '')
|
|
82
|
+
let filteredOptions: MultiSelectOption<T>[] = options
|
|
83
|
+
const enableSearch = !!search
|
|
84
|
+
if (enableSearch && !!searchText) {
|
|
85
|
+
filteredOptions = MultiSearchWithMapping<MultiSelectOption<T>>(
|
|
86
|
+
searchText,
|
|
87
|
+
filteredOptions,
|
|
88
|
+
value => search.searchMapping(value)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
if (!showDisabledOptions) {
|
|
92
|
+
filteredOptions = filteredOptions.filter(value => !value.disabled)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const selectedItems = options.filter(value => value.selected)
|
|
96
|
+
const menuButtonText = selectedItems.length === 0 ?
|
|
97
|
+
hintText ?? translation.select
|
|
98
|
+
: <span>{`${selectedItems.length} ${translation.selected}`}</span>
|
|
99
|
+
|
|
100
|
+
const borderColor = 'border-menu-border'
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className={clsx(className)}>
|
|
104
|
+
{label && (
|
|
105
|
+
<Label {...label} htmlFor={label.name} className={clsx(' mb-1', label.className)}
|
|
106
|
+
labelType={label.labelType ?? 'labelBig'}/>
|
|
107
|
+
)}
|
|
108
|
+
<Menu<HTMLDivElement>
|
|
109
|
+
alignment="t_"
|
|
110
|
+
trigger={(onClick, ref) => (
|
|
111
|
+
<div ref={ref} onClick={disabled ? undefined : onClick}
|
|
112
|
+
className={clsx(borderColor, 'bg-menu-background text-menu-text inline-w-full justify-between items-center rounded-lg border-2 px-4 py-2 font-medium',
|
|
113
|
+
{
|
|
114
|
+
'hover:brightness-90 hover:border-primary cursor-pointer': !disabled,
|
|
115
|
+
'bg-disabled-background text-disabled cursor-not-allowed': disabled
|
|
116
|
+
},
|
|
117
|
+
triggerClassName)}
|
|
118
|
+
>
|
|
119
|
+
{selectedDisplay ? selectedDisplay({ items: options, disabled }) : menuButtonText}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
menuClassName={clsx(
|
|
123
|
+
'!rounded-lg !shadow-lg !max-h-[500px] !min-w-[400px] !max-w-[70vh] !overflow-y-auto !border !border-2', borderColor,
|
|
124
|
+
{ '!py-0': !enableSearch, '!pb-0': enableSearch }
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{enableSearch && (
|
|
128
|
+
<div key="selectSearch" className="row gap-x-2 items-center px-2 py-2">
|
|
129
|
+
<Input autoFocus={true} value={searchText} onChange={setSearchText}/>
|
|
130
|
+
<Search/>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
{filteredOptions.map((option, index) => (
|
|
134
|
+
<MenuItem key={`item${index}`} className={clsx({
|
|
135
|
+
'cursor-not-allowed !bg-disabled-background !text-disabled-text hover:brightness-100': !!option.disabled,
|
|
136
|
+
'cursor-pointer': !option.disabled,
|
|
137
|
+
})}
|
|
138
|
+
>
|
|
139
|
+
<div
|
|
140
|
+
className={clsx('overflow-hidden whitespace-nowrap text-ellipsis row items-center gap-x-2', option.className)}
|
|
141
|
+
onClick={() => {
|
|
142
|
+
if (!option.disabled) {
|
|
143
|
+
onChange(options.map(value => value.value === option.value ? ({
|
|
144
|
+
...option,
|
|
145
|
+
selected: !value.selected
|
|
146
|
+
}) : value))
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<Checkbox checked={option.selected} disabled={option.disabled} size="small"/>
|
|
151
|
+
{option.label}
|
|
152
|
+
</div>
|
|
153
|
+
</MenuItem>
|
|
154
|
+
))}
|
|
155
|
+
</Menu>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|