@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.
Files changed (162) hide show
  1. package/.storybook/main.ts +24 -0
  2. package/.storybook/preview.tsx +67 -0
  3. package/LICENSE +373 -0
  4. package/README.md +8 -0
  5. package/coloring/shading.ts +46 -0
  6. package/coloring/types.ts +13 -0
  7. package/components/Avatar.tsx +58 -0
  8. package/components/AvatarGroup.tsx +48 -0
  9. package/components/BreadCrumb.tsx +35 -0
  10. package/components/Button.tsx +236 -0
  11. package/components/ChipList.tsx +89 -0
  12. package/components/Circle.tsx +27 -0
  13. package/components/ErrorComponent.tsx +40 -0
  14. package/components/Expandable.tsx +61 -0
  15. package/components/HelpwaveBadge.tsx +35 -0
  16. package/components/HideableContentSection.tsx +43 -0
  17. package/components/InputGroup.tsx +72 -0
  18. package/components/LoadingAndErrorComponent.tsx +47 -0
  19. package/components/LoadingAnimation.tsx +40 -0
  20. package/components/LoadingButton.tsx +27 -0
  21. package/components/MarkdownInterpreter.tsx +278 -0
  22. package/components/Pagination.tsx +65 -0
  23. package/components/Profile.tsx +124 -0
  24. package/components/ProgressIndicator.tsx +58 -0
  25. package/components/Ring.tsx +286 -0
  26. package/components/SearchableList.tsx +69 -0
  27. package/components/SortButton.tsx +33 -0
  28. package/components/Span.tsx +0 -0
  29. package/components/StepperBar.tsx +124 -0
  30. package/components/Table.tsx +330 -0
  31. package/components/TechRadar.tsx +247 -0
  32. package/components/TextImage.tsx +86 -0
  33. package/components/TimeDisplay.tsx +121 -0
  34. package/components/Tooltip.tsx +92 -0
  35. package/components/VerticalDivider.tsx +51 -0
  36. package/components/date/DatePicker.tsx +164 -0
  37. package/components/date/DayPicker.tsx +95 -0
  38. package/components/date/TimePicker.tsx +167 -0
  39. package/components/date/YearMonthPicker.tsx +130 -0
  40. package/components/examples/InputGroupExample.tsx +58 -0
  41. package/components/examples/MultiSelectExample.tsx +57 -0
  42. package/components/examples/SearchableSelectExample.tsx +34 -0
  43. package/components/examples/SelectExample.tsx +28 -0
  44. package/components/examples/StackingModals.tsx +54 -0
  45. package/components/examples/TableExample.tsx +159 -0
  46. package/components/examples/TextareaExample.tsx +23 -0
  47. package/components/examples/TileExample.tsx +25 -0
  48. package/components/examples/Title.tsx +0 -0
  49. package/components/examples/date/DateTimePickerExample.tsx +53 -0
  50. package/components/examples/properties/CheckboxPropertyExample.tsx +29 -0
  51. package/components/examples/properties/DatePropertyExample.tsx +44 -0
  52. package/components/examples/properties/MultiSelectPropertyExample.tsx +39 -0
  53. package/components/examples/properties/NumberPropertyExample.tsx +28 -0
  54. package/components/examples/properties/SelectPropertyExample.tsx +39 -0
  55. package/components/examples/properties/TextPropertyExample.tsx +30 -0
  56. package/components/icons/Helpwave.tsx +51 -0
  57. package/components/icons/Tag.tsx +29 -0
  58. package/components/layout/Carousel.tsx +396 -0
  59. package/components/layout/DividerInserter.tsx +37 -0
  60. package/components/layout/FAQSection.tsx +57 -0
  61. package/components/layout/Tile.tsx +67 -0
  62. package/components/modals/ConfirmDialog.tsx +105 -0
  63. package/components/modals/DiscardChangesDialog.tsx +71 -0
  64. package/components/modals/InputModal.tsx +26 -0
  65. package/components/modals/LanguageModal.tsx +76 -0
  66. package/components/modals/Modal.tsx +149 -0
  67. package/components/modals/ModalRegister.tsx +45 -0
  68. package/components/properties/CheckboxProperty.tsx +62 -0
  69. package/components/properties/DateProperty.tsx +58 -0
  70. package/components/properties/MultiSelectProperty.tsx +82 -0
  71. package/components/properties/NumberProperty.tsx +86 -0
  72. package/components/properties/PropertyBase.tsx +84 -0
  73. package/components/properties/SelectProperty.tsx +67 -0
  74. package/components/properties/TextProperty.tsx +81 -0
  75. package/components/user-input/Checkbox.tsx +139 -0
  76. package/components/user-input/DateAndTimePicker.tsx +156 -0
  77. package/components/user-input/Input.tsx +192 -0
  78. package/components/user-input/Label.tsx +32 -0
  79. package/components/user-input/Menu.tsx +75 -0
  80. package/components/user-input/MultiSelect.tsx +158 -0
  81. package/components/user-input/ScrollPicker.tsx +240 -0
  82. package/components/user-input/SearchableSelect.tsx +36 -0
  83. package/components/user-input/Select.tsx +132 -0
  84. package/components/user-input/Textarea.tsx +86 -0
  85. package/components/user-input/ToggleableInput.tsx +115 -0
  86. package/eslint.config.js +3 -0
  87. package/globals.css +488 -0
  88. package/hooks/useHoverState.ts +88 -0
  89. package/hooks/useLanguage.tsx +78 -0
  90. package/hooks/useLocalStorage.tsx +33 -0
  91. package/hooks/useOutsideClick.ts +25 -0
  92. package/hooks/useSaveDelay.ts +46 -0
  93. package/hooks/useTheme.tsx +57 -0
  94. package/hooks/useTranslation.ts +43 -0
  95. package/index.ts +0 -0
  96. package/package.json +71 -0
  97. package/postcss.config.mjs +7 -0
  98. package/stories/README.md +23 -0
  99. package/stories/coloring/shading.stories.tsx +54 -0
  100. package/stories/geometry/Circle.stories.tsx +16 -0
  101. package/stories/geometry/rings/AnimatedRing.stories.tsx +18 -0
  102. package/stories/geometry/rings/RadialRings.stories.tsx +19 -0
  103. package/stories/geometry/rings/Ring.stories.tsx +17 -0
  104. package/stories/geometry/rings/RingWave.stories.tsx +20 -0
  105. package/stories/layout/FAQSection.stories.tsx +49 -0
  106. package/stories/layout/InputGroup.stories.tsx +19 -0
  107. package/stories/layout/Table.stories.tsx +19 -0
  108. package/stories/layout/TextImage.stories.tsx +24 -0
  109. package/stories/layout/chip/Chip.stories.tsx +19 -0
  110. package/stories/layout/chip/ChipList.stories.tsx +27 -0
  111. package/stories/layout/tile/Tile.stories.ts +20 -0
  112. package/stories/layout/tile/TileWithImage.stories.tsx +27 -0
  113. package/stories/other/BreadCrumbs.stories.tsx +21 -0
  114. package/stories/other/HelpwaveBadge.stories.tsx +18 -0
  115. package/stories/other/HelpwaveSpinner.stories.tsx +19 -0
  116. package/stories/other/MarkdownInterpreter.stories.tsx +18 -0
  117. package/stories/other/Profile.stories.tsx +52 -0
  118. package/stories/other/SearchableList.stories.tsx +21 -0
  119. package/stories/other/StackingModals.stories.tsx +16 -0
  120. package/stories/other/TechRadar.stories.tsx +14 -0
  121. package/stories/other/Translation.stories.tsx +56 -0
  122. package/stories/other/VerticalDivider.stories.tsx +20 -0
  123. package/stories/other/avatar/Avatar.stories.tsx +19 -0
  124. package/stories/other/avatar/AvatarGroup.stories.tsx +26 -0
  125. package/stories/other/tooltip/Tooltip.stories.tsx +30 -0
  126. package/stories/other/tooltip/TooltipStack.stories.tsx +39 -0
  127. package/stories/user-action/button/LoadingButton.stories.tsx +21 -0
  128. package/stories/user-action/button/OutlineButton.stories.tsx +22 -0
  129. package/stories/user-action/button/SolidButton.stories.tsx +22 -0
  130. package/stories/user-action/button/TextButton.stories.tsx +22 -0
  131. package/stories/user-action/input/Checkbox.stories.tsx +20 -0
  132. package/stories/user-action/input/Label.stories.tsx +18 -0
  133. package/stories/user-action/input/ScrollPicker.stories.tsx +20 -0
  134. package/stories/user-action/input/Textarea.stories.tsx +22 -0
  135. package/stories/user-action/input/date/DatePicker.stories.tsx +23 -0
  136. package/stories/user-action/input/date/DateTimePicker.stories.tsx +26 -0
  137. package/stories/user-action/input/date/DayPicker.stories.tsx +20 -0
  138. package/stories/user-action/input/date/TimePicker.stories.tsx +20 -0
  139. package/stories/user-action/input/date/YearMonthPicker.stories.tsx +21 -0
  140. package/stories/user-action/input/select/MultiSelect.stories.tsx +39 -0
  141. package/stories/user-action/input/select/SearchableSelect.stories.tsx +32 -0
  142. package/stories/user-action/input/select/Select.stories.tsx +30 -0
  143. package/stories/user-action/properties/CheckboxProperty.stories.tsx +20 -0
  144. package/stories/user-action/properties/DateProperty.stories.tsx +21 -0
  145. package/stories/user-action/properties/MultiSelectProperty.stories.tsx +33 -0
  146. package/stories/user-action/properties/NumberProperty.stories.tsx +21 -0
  147. package/stories/user-action/properties/PropertyBase.stories.tsx +28 -0
  148. package/stories/user-action/properties/SingleSelectProperty.stories.tsx +35 -0
  149. package/stories/user-action/properties/TextProperty.stories.tsx +20 -0
  150. package/tsconfig.json +20 -0
  151. package/util/array.ts +115 -0
  152. package/util/builder.ts +9 -0
  153. package/util/date.ts +180 -0
  154. package/util/easeFunctions.ts +37 -0
  155. package/util/emailValidation.ts +3 -0
  156. package/util/loopingArray.ts +94 -0
  157. package/util/math.ts +3 -0
  158. package/util/news.ts +43 -0
  159. package/util/noop.ts +1 -0
  160. package/util/simpleSearch.ts +65 -0
  161. package/util/storage.ts +37 -0
  162. package/util/types.ts +4 -0
@@ -0,0 +1,286 @@
1
+ import type { CSSProperties } from 'react'
2
+ import { useCallback, useEffect, useState } from 'react'
3
+ import { noop } from '../util/noop'
4
+ import { Circle } from './Circle'
5
+ import clsx from 'clsx'
6
+
7
+ export type RingProps = {
8
+ innerSize: number, // the size of the entire circle including the circleWidth
9
+ width: number,
10
+ className?: string,
11
+ };
12
+
13
+ export const Ring = ({
14
+ innerSize = 20,
15
+ width = 7,
16
+ className = 'outline-primary',
17
+ }: RingProps) => {
18
+ return (
19
+ <div
20
+ className={clsx(`bg-transparent rounded-full outline`, className)}
21
+ style={{
22
+ width: `${innerSize}px`,
23
+ height: `${innerSize}px`,
24
+ outlineWidth: `${width}px`,
25
+ }}
26
+ />
27
+ )
28
+ }
29
+
30
+ export type AnimatedRingProps = RingProps & {
31
+ fillAnimationDuration?: number, // in seconds, 0 means no animation
32
+ repeating?: boolean,
33
+ onAnimationFinished?: () => void,
34
+ style?: CSSProperties,
35
+ };
36
+
37
+ export const AnimatedRing = ({
38
+ innerSize,
39
+ width,
40
+ className,
41
+ fillAnimationDuration = 3,
42
+ repeating = false,
43
+ onAnimationFinished = noop,
44
+ style,
45
+ }: AnimatedRingProps) => {
46
+ const [currentWidth, setCurrentWidth] = useState(0)
47
+ const milliseconds = 1000 * fillAnimationDuration
48
+
49
+ const animate = useCallback((timestamp: number, startTime: number) => {
50
+ const progress = Math.min((timestamp - startTime) / milliseconds, 1)
51
+ const newWidth = Math.min(width * progress, width)
52
+
53
+ setCurrentWidth(newWidth)
54
+
55
+ if (progress < 1) {
56
+ requestAnimationFrame((newTimestamp) => animate(newTimestamp, startTime))
57
+ } else {
58
+ onAnimationFinished()
59
+ if (repeating) {
60
+ setCurrentWidth(0)
61
+ requestAnimationFrame((newTimestamp) => animate(newTimestamp, newTimestamp))
62
+ }
63
+ }
64
+ }, [milliseconds, onAnimationFinished, repeating, width])
65
+
66
+ useEffect(() => {
67
+ if (currentWidth < width) {
68
+ requestAnimationFrame((timestamp) => animate(timestamp, timestamp))
69
+ }
70
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
71
+
72
+ return (
73
+ <div
74
+ className="row items-center justify-center"
75
+ style={{
76
+ width: `${innerSize + 2 * width}px`,
77
+ height: `${innerSize + 2 * width}px`,
78
+ ...style,
79
+ }}
80
+ >
81
+ <Ring
82
+ innerSize={innerSize}
83
+ width={currentWidth}
84
+ className={className}
85
+ />
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export type RingWaveProps = Omit<AnimatedRingProps, 'innerSize'> & {
91
+ startInnerSize: number,
92
+ endInnerSize: number,
93
+ style?: CSSProperties,
94
+ };
95
+
96
+ export const RingWave = ({
97
+ startInnerSize = 20,
98
+ endInnerSize = 30,
99
+ width,
100
+ className,
101
+ fillAnimationDuration = 3,
102
+ repeating = false,
103
+ onAnimationFinished = noop,
104
+ style
105
+ }: RingWaveProps) => {
106
+ const [currentInnerSize, setCurrentInnerSize] = useState(startInnerSize)
107
+ const distance = endInnerSize - startInnerSize
108
+ const milliseconds = 1000 * fillAnimationDuration
109
+
110
+ const animate = useCallback((timestamp: number, startTime: number) => {
111
+ const progress = Math.min((timestamp - startTime) / milliseconds, 1)
112
+ const newInnerSize = Math.min(
113
+ startInnerSize + distance * progress,
114
+ endInnerSize
115
+ )
116
+
117
+ setCurrentInnerSize(newInnerSize)
118
+
119
+ if (progress < 1) {
120
+ requestAnimationFrame((newTimestamp) => animate(newTimestamp, startTime))
121
+ } else {
122
+ onAnimationFinished()
123
+ if (repeating) {
124
+ setCurrentInnerSize(startInnerSize)
125
+ requestAnimationFrame((newTimestamp) => animate(newTimestamp, newTimestamp))
126
+ }
127
+ }
128
+ }, [distance, endInnerSize, milliseconds, onAnimationFinished, repeating, startInnerSize])
129
+
130
+ useEffect(() => {
131
+ if (currentInnerSize < endInnerSize) {
132
+ requestAnimationFrame((timestamp) => animate(timestamp, timestamp))
133
+ }
134
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
135
+
136
+ return (
137
+ <div
138
+ className="row items-center justify-center"
139
+ style={{
140
+ width: `${endInnerSize + 2 * width}px`,
141
+ height: `${endInnerSize + 2 * width}px`,
142
+ ...style
143
+ }}
144
+ >
145
+ <Ring
146
+ innerSize={currentInnerSize}
147
+ width={width}
148
+ className={className}
149
+ />
150
+ </div>
151
+ )
152
+ }
153
+
154
+ export type RadialRingsProps = {
155
+ circle1ClassName?: string,
156
+ circle2ClassName?: string,
157
+ circle3ClassName?: string,
158
+ waveWidth?: number,
159
+ waveBaseColor?: string,
160
+ sizeCircle1?: number,
161
+ sizeCircle2?: number,
162
+ sizeCircle3?: number,
163
+ };
164
+
165
+ // TODO use fixed colors here to avoid artifacts
166
+ export const RadialRings = ({
167
+ circle1ClassName = 'bg-primary/90 outline-primary/90',
168
+ circle2ClassName = 'bg-primary/60 outline-primary/60',
169
+ circle3ClassName = 'bg-primary/40 outline-primary/40',
170
+ waveWidth = 10,
171
+ waveBaseColor = 'outline-white/20',
172
+ sizeCircle1 = 100,
173
+ sizeCircle2 = 200,
174
+ sizeCircle3 = 300
175
+ }: RadialRingsProps) => {
176
+ const [currentRing, setCurrentRing] = useState(0)
177
+ const size = sizeCircle3
178
+
179
+ return (
180
+ <div
181
+ className="relative"
182
+ style={{
183
+ width: `${sizeCircle3}px`,
184
+ height: `${sizeCircle3}px`,
185
+ }}
186
+ >
187
+ <Circle
188
+ radius={sizeCircle1 / 2}
189
+ className={clsx(circle1ClassName, `absolute z-[10] -translate-y-1/2 -translate-x-1/2`)}
190
+ style={{
191
+ left: `${size / 2}px`,
192
+ top: `${size / 2}px`
193
+ }}
194
+ />
195
+ {currentRing === 0 ? (
196
+ <AnimatedRing
197
+ innerSize={sizeCircle1}
198
+ width={(sizeCircle2 - sizeCircle1) / 2}
199
+ onAnimationFinished={() =>
200
+ currentRing === 0 ? setCurrentRing(1) : null
201
+ }
202
+ repeating={true}
203
+ className={clsx(circle2ClassName,
204
+ { 'opacity-5': currentRing !== 0 })}
205
+ style={{
206
+ left: `${size / 2}px`,
207
+ top: `${size / 2}px`,
208
+ position: 'absolute',
209
+ translate: `-50% -50%`,
210
+ zIndex: 9
211
+ }}
212
+ />
213
+ ) : null}
214
+ {currentRing === 2 ? (
215
+ <RingWave
216
+ startInnerSize={sizeCircle1 - waveWidth}
217
+ endInnerSize={sizeCircle2}
218
+ width={waveWidth}
219
+ repeating={true}
220
+ className={clsx(waveBaseColor, `opacity-5`)}
221
+ style={{
222
+ left: `${size / 2}px`,
223
+ top: `${size / 2}px`,
224
+ position: 'absolute',
225
+ translate: `-50% -50%`,
226
+ zIndex: 9,
227
+ }}
228
+ />
229
+ ) : null}
230
+ <Circle
231
+ radius={sizeCircle2 / 2}
232
+ className={clsx(circle2ClassName,
233
+ { 'opacity-20': currentRing < 1 },
234
+ `absolute z-[8] -translate-y-1/2 -translate-x-1/2`)}
235
+ style={{
236
+ left: `${size / 2}px`,
237
+ top: `${size / 2}px`
238
+ }}
239
+ />
240
+ {currentRing === 1 ? (
241
+ <AnimatedRing
242
+ innerSize={sizeCircle2 - 1} // potentially harmful
243
+ width={(sizeCircle3 - sizeCircle2) / 2}
244
+ onAnimationFinished={() =>
245
+ currentRing === 1 ? setCurrentRing(2) : null
246
+ }
247
+ repeating={true}
248
+ className={clsx(circle3ClassName)}
249
+ style={{
250
+ left: `${size / 2}px`,
251
+ top: `${size / 2}px`,
252
+ position: 'absolute',
253
+ translate: `-50% -50%`,
254
+ zIndex: 7,
255
+ }}
256
+ />
257
+ ) : null}
258
+ {currentRing === 2 ? (
259
+ <RingWave
260
+ startInnerSize={sizeCircle2}
261
+ endInnerSize={sizeCircle3 - waveWidth}
262
+ width={waveWidth}
263
+ repeating={true}
264
+ className={clsx(waveBaseColor, `opacity-5`)}
265
+ style={{
266
+ left: `${size / 2}px`,
267
+ top: `${size / 2}px`,
268
+ position: 'absolute',
269
+ translate: `-50% -50%`,
270
+ zIndex: 7,
271
+ }}
272
+ />
273
+ ) : null}
274
+ <Circle
275
+ radius={sizeCircle3 / 2}
276
+ className={clsx(circle3ClassName,
277
+ { 'opacity-20': currentRing < 2 },
278
+ `absolute z-[6] -translate-y-1/2 -translate-x-1/2`)}
279
+ style={{
280
+ left: `${size / 2}px`,
281
+ top: `${size / 2}px`
282
+ }}
283
+ />
284
+ </div>
285
+ )
286
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { Search } from 'lucide-react'
4
+ import clsx from 'clsx'
5
+ import type { Languages } from '../hooks/useLanguage'
6
+ import { useTranslation } from '../hooks/useTranslation'
7
+ import type { PropsForTranslation } from '../hooks/useTranslation'
8
+ import { MultiSearchWithMapping } from '../util/simpleSearch'
9
+ import { Input } from './user-input/Input'
10
+
11
+ type SearchableListTranslation = {
12
+ search: string,
13
+ nothingFound: string,
14
+ }
15
+
16
+ const defaultSearchableListTranslation: Record<Languages, SearchableListTranslation> = {
17
+ en: {
18
+ search: 'Search',
19
+ nothingFound: 'Nothing found'
20
+ },
21
+ de: {
22
+ search: 'Suche',
23
+ nothingFound: 'Nichts gefunden'
24
+ }
25
+ }
26
+
27
+ export type SearchableListProps<T> = {
28
+ list: T[],
29
+ initialSearch?: string,
30
+ searchMapping: (value: T) => string[],
31
+ itemMapper: (value: T) => ReactNode,
32
+ className?: string,
33
+ }
34
+
35
+ /**
36
+ * A component for searching a list
37
+ */
38
+ export const SearchableList = <T, >({
39
+ overwriteTranslation,
40
+ list,
41
+ initialSearch = '',
42
+ searchMapping,
43
+ itemMapper,
44
+ className
45
+ }: PropsForTranslation<SearchableListTranslation, SearchableListProps<T>>) => {
46
+ const translation = useTranslation(defaultSearchableListTranslation, overwriteTranslation)
47
+ const [search, setSearch] = useState<string>(initialSearch)
48
+
49
+ useEffect(() => setSearch(initialSearch), [initialSearch])
50
+
51
+ const filteredEntries = useMemo(() => MultiSearchWithMapping(search, list, searchMapping), [search, list, searchMapping])
52
+
53
+ return (
54
+ <div className={clsx('col gap-y-2', className)}>
55
+ <div className="row justify-between gap-x-2 items-center">
56
+ <div className="flex-1">
57
+ <Input value={search} onChange={setSearch} placeholder={translation.search}/>
58
+ </div>
59
+ <Search size={20}/>
60
+ </div>
61
+ {filteredEntries.length > 0 && (
62
+ <div className="col gap-y-1">
63
+ {filteredEntries.map(itemMapper)}
64
+ </div>
65
+ )}
66
+ {!filteredEntries.length && <div className="row justify-center">{translation.nothingFound}</div>}
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,33 @@
1
+ import { ChevronDown, ChevronsUpDown, ChevronUp } from 'lucide-react'
2
+ import type { TextButtonProps } from './Button'
3
+ import { TextButton } from './Button'
4
+ import type { TableSortingType } from './Table'
5
+
6
+ export type SortButtonProps = Omit<TextButtonProps, 'onClick'> & {
7
+ ascending?: TableSortingType,
8
+ onClick: (newTableSorting:TableSortingType) => void,
9
+ }
10
+
11
+ /**
12
+ * A Extension of the normal button that displays the sorting state right of the content
13
+ */
14
+ export const SortButton = ({
15
+ children,
16
+ ascending,
17
+ color,
18
+ onClick,
19
+ ...buttonProps
20
+ }: SortButtonProps) => {
21
+ return (
22
+ <TextButton
23
+ color={color}
24
+ onClick={() => onClick(ascending === 'descending' ? 'ascending' : 'descending')}
25
+ {...buttonProps}
26
+ >
27
+ <div className="row gap-x-2">
28
+ {children}
29
+ {ascending === 'ascending' ? <ChevronUp/> : (!ascending ? <ChevronsUpDown/> : <ChevronDown/>)}
30
+ </div>
31
+ </TextButton>
32
+ )
33
+ }
File without changes
@@ -0,0 +1,124 @@
1
+ import { Check, ChevronLeft, ChevronRight } from 'lucide-react'
2
+ import type { Languages } from '../hooks/useLanguage'
3
+ import type { PropsForTranslation } from '../hooks/useTranslation'
4
+ import { useTranslation } from '../hooks/useTranslation'
5
+ import { range } from '../util/array'
6
+ import { SolidButton } from './Button'
7
+ import clsx from 'clsx'
8
+
9
+ type StepperBarTranslation = {
10
+ back: string,
11
+ next: string,
12
+ confirm: string,
13
+ }
14
+
15
+ const defaultStepperBarTranslation: Record<Languages, StepperBarTranslation> = {
16
+ en: {
17
+ back: 'Back',
18
+ next: 'Next Step',
19
+ confirm: 'Create'
20
+ },
21
+ de: {
22
+ back: 'Zurück',
23
+ next: 'Nächster Schritt',
24
+ confirm: 'Erstellen'
25
+ }
26
+ }
27
+
28
+ export type StepperInformation = {
29
+ step: number,
30
+ lastStep: number,
31
+ seenSteps: Set<number>,
32
+ }
33
+
34
+ export type StepperBarProps = {
35
+ stepper: StepperInformation,
36
+ onChange: (step: StepperInformation) => void,
37
+ onFinish: () => void,
38
+ showDots?: boolean,
39
+ className?: string,
40
+ }
41
+
42
+ /**
43
+ * A Component for stepping
44
+ */
45
+ export const StepperBar = ({
46
+ overwriteTranslation,
47
+ stepper,
48
+ onChange,
49
+ onFinish,
50
+ showDots = true,
51
+ className = '',
52
+ }: PropsForTranslation<StepperBarTranslation, StepperBarProps>) => {
53
+ const translation = useTranslation(defaultStepperBarTranslation, overwriteTranslation)
54
+ const dots = range(0, stepper.lastStep)
55
+ const { step, seenSteps, lastStep } = stepper
56
+
57
+ const update = (newStep: number) => {
58
+ seenSteps.add(newStep)
59
+ onChange({ step: newStep, seenSteps, lastStep })
60
+ }
61
+
62
+ return (
63
+ <div
64
+ className={clsx('sticky row p-2 border-2 justify-between rounded-lg shadow-lg', className)}
65
+ >
66
+ <div className="flex-[2] justify-start">
67
+ <SolidButton
68
+ disabled={step === 0}
69
+ onClick={() => {
70
+ update(step - 1)
71
+ }}
72
+ className="row gap-x-1 items-center justify-center"
73
+ >
74
+ <ChevronLeft size={14}/>
75
+ {translation.back}
76
+ </SolidButton>
77
+ </div>
78
+ <div className="row flex-[5] gap-x-2 justify-center items-center">
79
+ {showDots && dots.map((value, index) => {
80
+ const seen = seenSteps.has(index)
81
+ return (
82
+ <div
83
+ key={index}
84
+ onClick={() => seen && update(index)}
85
+ className={clsx('rounded-full w-4 h-4', {
86
+ 'bg-primary hover:brightness-75': index === step && seen,
87
+ 'bg-primary/40 hover:bg-primary': index !== step && seen,
88
+ 'bg-gray-200 outline-transparent': !seen,
89
+ },
90
+ {
91
+ 'cursor-pointer': seen,
92
+ 'cursor-not-allowed': !seen,
93
+ })}
94
+ />
95
+ )
96
+ })}
97
+ </div>
98
+ {step !== lastStep && (
99
+ <div className="flex-[2] justify-end">
100
+ <SolidButton
101
+ onClick={() => update(step + 1)}
102
+ className="row gap-x-1 items-center justify-center"
103
+ >
104
+ {translation.next}
105
+ <ChevronRight size={14}/>
106
+ </SolidButton>
107
+ </div>
108
+ )}
109
+ {step === lastStep && (
110
+ <div className="flex-[2] justify-end">
111
+ <SolidButton
112
+ // TODO check form validity
113
+ disabled={false}
114
+ onClick={onFinish}
115
+ className="row gap-x-1 items-center justify-center"
116
+ >
117
+ <Check size={14}/>
118
+ {translation.confirm}
119
+ </SolidButton>
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }