@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,39 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { MultiSelectExample } from '../../../../components/examples/MultiSelectExample'
3
+ import { action } from '@storybook/addon-actions'
4
+
5
+ const meta = {
6
+ title: 'User-Action/Input/Select',
7
+ component: MultiSelectExample,
8
+ } satisfies Meta<typeof MultiSelectExample>
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const MultiSelectVariations: Story = {
14
+ args: {
15
+ label: { name: 'Select-Label' },
16
+ options: [
17
+ { value: '1', selected: false, label: 'Entry 1' },
18
+ { value: '2', selected: false, label: 'Entry 2', disabled: true },
19
+ { value: '3', selected: false, label: 'Different Entry 3' },
20
+ { value: '4', selected: false, label: 'Custom styled Entry 4', className: '!text-red-400' },
21
+ { value: '5', selected: false, label: 'Entry 5' },
22
+ { value: '6', selected: false, label: 'Entry 6', disabled: true },
23
+ { value: '7', selected: false, label: 'Long Entry 7' },
24
+ { value: '8', selected: false, label: 'Long Entry 8' },
25
+ { value: '9', selected: false, label: 'Very Long Entry 9' },
26
+ { value: '10', selected: false, label: 'Long Entry 10' },
27
+ { value: '11', selected: false, label: 'Very very Long Entry 11' },
28
+ { value: '12', selected: false, label: 'Entry 12', disabled: true }
29
+ ],
30
+ disabled: false,
31
+ hintText: undefined,
32
+ showDisabledOptions: true,
33
+ useChipDisplay: true,
34
+ enableSearch: false,
35
+ className: '',
36
+ triggerClassName: '',
37
+ onChange: action('onChange'),
38
+ },
39
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { SearchableSelectExample } from '../../../../components/examples/SearchableSelectExample'
3
+ import { action } from '@storybook/addon-actions'
4
+
5
+ const meta = {
6
+ title: 'User-Action/Input/Select',
7
+ component: SearchableSelectExample,
8
+ } satisfies Meta<typeof SearchableSelectExample>
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const SearchableSelectVariations: Story = {
14
+ args: {
15
+ label: { name: 'Select-Label', labelType: 'labelMedium' },
16
+ value: undefined,
17
+ options: [
18
+ { value: 'Entry 1', label: 'Entry 1' },
19
+ { value: 'Entry 2', label: 'Entry 2', disabled: true },
20
+ { value: 'Entry 3', label: 'Entry 3' },
21
+ { value: 'Custom styled', label: <span className="!text-red-400">Custom styled</span> },
22
+ { value: 'Entry 5', label: 'Entry 5' },
23
+ { value: 'Entry 6', label: 'Entry 6', disabled: true }
24
+ ],
25
+ isDisabled: false,
26
+ hintText: 'Hinttext',
27
+ isHidingCurrentValue: false,
28
+ showDisabledOptions: true,
29
+ className: '',
30
+ onChange: action('onChange'),
31
+ },
32
+ }
@@ -0,0 +1,30 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { SelectExample } from '../../../../components/examples/SelectExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Input/Select',
6
+ component: SelectExample<string>,
7
+ } satisfies Meta<typeof SelectExample<string>>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const SelectVariations: Story = {
13
+ args: {
14
+ label: { name: 'Select-Label', labelType: 'labelMedium' },
15
+ value: undefined,
16
+ options: [
17
+ { value: '1', label: 'Entry 1' },
18
+ { value: '2', label: 'Entry 2', disabled: true },
19
+ { value: '3', label: 'Entry 3' },
20
+ { value: '4', label: <span className="!text-red-400">Custom styled</span> },
21
+ { value: '5', label: 'Entry 5' },
22
+ { value: '6', label: 'Entry 6', disabled: true }
23
+ ],
24
+ isDisabled: false,
25
+ hintText: 'Hinttext',
26
+ isHidingCurrentValue: false,
27
+ showDisabledOptions: true,
28
+ className: '',
29
+ },
30
+ }
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { CheckboxPropertyExample } from '../../../components/examples/properties/CheckboxPropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: CheckboxPropertyExample,
7
+ } satisfies Meta<typeof CheckboxPropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const CheckboxPropertyVariation: Story = {
13
+ args: {
14
+ name: 'Property',
15
+ softRequired: false,
16
+ value: false,
17
+ readOnly: false,
18
+ className: '',
19
+ },
20
+ }
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { DatePropertyExample } from '../../../components/examples/properties/DatePropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: DatePropertyExample,
7
+ } satisfies Meta<typeof DatePropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const DatePropertyVariation: Story = {
13
+ args: {
14
+ name: 'Property',
15
+ softRequired: false,
16
+ value: undefined,
17
+ readOnly: false,
18
+ type: 'dateTime',
19
+ className: '',
20
+ },
21
+ }
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { MultiSelectPropertyExample } from '../../../components/examples/properties/MultiSelectPropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: MultiSelectPropertyExample,
7
+ } satisfies Meta<typeof MultiSelectPropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const MultiSelectPropertyVariation: Story = {
13
+ args: {
14
+ name: 'Fruits',
15
+ softRequired: false,
16
+ options: [
17
+ { value: 'apple', label: 'Apple', selected: false },
18
+ { value: 'pear', label: 'Pear', selected: false },
19
+ { value: 'plum', label: 'Plum', selected: false },
20
+ { value: 'strawberry', label: 'Strawberry', selected: false, disabled: true },
21
+ { value: 'orange', label: 'Orange', selected: false },
22
+ { value: 'maracuja', label: 'Maracuja', selected: false },
23
+ { value: 'lemon', label: 'Lemon', selected: false },
24
+ { value: 'pineapple', label: 'Pineapple', selected: false },
25
+ { value: 'kiwi', label: 'Kiwi', selected: false },
26
+ { value: 'watermelon', label: 'Watermelon', selected: false },
27
+ ],
28
+ readOnly: false,
29
+ hintText: 'Select',
30
+ enableSearch: true,
31
+ showDisabledOptions: true
32
+ },
33
+ }
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { NumberPropertyExample } from '../../../components/examples/properties/NumberPropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: NumberPropertyExample,
7
+ } satisfies Meta<typeof NumberPropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const NumberPropertyVariation: Story = {
13
+ args: {
14
+ name: 'Property',
15
+ softRequired: false,
16
+ value: undefined,
17
+ suffix: 'kg',
18
+ readOnly: false,
19
+ className: '',
20
+ },
21
+ }
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import clsx from 'clsx'
3
+ import { PropertyBase } from '../../../components/properties/PropertyBase'
4
+
5
+ const meta = {
6
+ title: 'User-Action/Property',
7
+ component: PropertyBase,
8
+ } satisfies Meta<typeof PropertyBase>
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const PropertyBaseVariation: Story = {
14
+ args: {
15
+ name: 'Property',
16
+ softRequired: false,
17
+ hasValue: true,
18
+ input: ({ softRequired, hasValue }) => (
19
+ <div
20
+ className={clsx('row grow py-2 px-4', { 'text-warning': softRequired && !hasValue })}
21
+ >
22
+ Value
23
+ </div>
24
+ ),
25
+ className: '',
26
+ readOnly: false,
27
+ },
28
+ }
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { SingleSelectPropertyExample } from '../../../components/examples/properties/SelectPropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: SingleSelectPropertyExample,
7
+ } satisfies Meta<typeof SingleSelectPropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const SingleSelectPropertyVariation: Story = {
13
+ args: {
14
+ value: undefined,
15
+ name: 'Fruits',
16
+ softRequired: false,
17
+ options: [
18
+ { value: 'apple', label: 'Apple' },
19
+ { value: 'pear', label: 'Pear' },
20
+ { value: 'plum', label: 'Plum' },
21
+ { value: 'strawberry', label: 'Strawberry', disabled: true },
22
+ { value: 'orange', label: 'Orange' },
23
+ { value: 'maracuja', label: 'Maracuja' },
24
+ { value: 'lemon', label: 'Lemon' },
25
+ { value: 'pineapple', label: 'Pineapple' },
26
+ { value: 'kiwi', label: 'Kiwi' },
27
+ { value: 'watermelon', label: 'Watermelon' },
28
+ ],
29
+ readOnly: false,
30
+ hintText: 'Select',
31
+ showDisabledOptions: true,
32
+ isDisabled: false,
33
+ isHidingCurrentValue: true
34
+ },
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { TextPropertyExample } from '../../../components/examples/properties/TextPropertyExample'
3
+
4
+ const meta = {
5
+ title: 'User-Action/Property',
6
+ component: TextPropertyExample,
7
+ } satisfies Meta<typeof TextPropertyExample>
8
+
9
+ export default meta
10
+ type Story = StoryObj<typeof meta>;
11
+
12
+ export const TextPropertyVariation: Story = {
13
+ args: {
14
+ name: 'Property',
15
+ softRequired: false,
16
+ value: undefined,
17
+ readOnly: false,
18
+ className: '',
19
+ },
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "node",
6
+ "allowJs": true,
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "react-jsx",
14
+ "incremental": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ "noPropertyAccessFromIndexSignature": true,
18
+ "noUncheckedIndexedAccess": true
19
+ }
20
+ }
package/util/array.ts ADDED
@@ -0,0 +1,115 @@
1
+ export const equalSizeGroups = <T >(array: T[], groupSize: number): T[][] => {
2
+ if (groupSize <= 0) {
3
+ console.warn(`group size should be greater than 0: groupSize = ${groupSize}`)
4
+ return [[...array]]
5
+ }
6
+
7
+ const groups = []
8
+ for (let i = 0; i < array.length; i += groupSize) {
9
+ groups.push(array.slice(i, Math.min(i + groupSize, array.length)))
10
+ }
11
+ return groups
12
+ }
13
+
14
+ /**
15
+ * @param start
16
+ * @param end inclusive
17
+ * @param allowEmptyRange Whether the range can be defined empty via end < start
18
+ */
19
+ export const range = (start: number, end: number, allowEmptyRange: boolean = false): number[] => {
20
+ if (end < start) {
21
+ if (!allowEmptyRange) {
22
+ console.warn(`range: end (${end}) < start (${start}) should be allowed explicitly, set allowEmptyRange to true`)
23
+ }
24
+ return []
25
+ }
26
+ return Array.from({ length: end - start + 1 }, (_, index) => index + start)
27
+ }
28
+
29
+ /** Finds the closest match
30
+ * @param list The list of all possible matches
31
+ * @param firstCloser Return whether item1 is closer than item2
32
+ */
33
+ export const closestMatch = <T >(list: T[], firstCloser: (item1: T, item2: T) => boolean) => {
34
+ return list.reduce((item1, item2) => {
35
+ return firstCloser(item1, item2) ? item1 : item2
36
+ })
37
+ }
38
+
39
+ /**
40
+ * returns the item in middle of a list and its neighbours before and after
41
+ * e.g. [1,2,3,4,5,6] for item = 1 would return [5,6,1,2,3]
42
+ */
43
+ export const getNeighbours = <T >(list: T[], item: T, neighbourDistance: number = 2) => {
44
+ const index = list.indexOf(item)
45
+ const totalItems = neighbourDistance * 2 + 1
46
+ if (list.length < totalItems) {
47
+ console.warn('List is to short')
48
+ return list
49
+ }
50
+
51
+ if (index === -1) {
52
+ console.error('item not found in list')
53
+ return list.splice(0, totalItems)
54
+ }
55
+
56
+ let start = index - neighbourDistance
57
+ if (start < 0) {
58
+ start += list.length
59
+ }
60
+ const end = (index + neighbourDistance + 1) % list.length
61
+
62
+ const result: T[] = []
63
+ let ignoreOnce = list.length === totalItems
64
+ for (let i = start; i !== end || ignoreOnce; i = (i + 1) % list.length) {
65
+ result.push(list[i]!)
66
+ if (end === i && ignoreOnce) {
67
+ ignoreOnce = false
68
+ }
69
+ }
70
+ return result
71
+ }
72
+
73
+ export const createLoopingListWithIndex = <T >(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {
74
+ if (length < 0) {
75
+ console.warn(`createLoopingList: length must be >= 0, given ${length}`)
76
+ } else if (length === 0) {
77
+ length = list.length
78
+ }
79
+
80
+ const returnList: [number, T][] = []
81
+
82
+ if (forwards) {
83
+ for (let i = startIndex; returnList.length < length; i = (i + 1) % list.length) {
84
+ returnList.push([i, list[i]!])
85
+ }
86
+ } else {
87
+ for (let i = startIndex; returnList.length < length; i = i === 0 ? i = list.length - 1 : i - 1) {
88
+ returnList.push([i, list[i]!])
89
+ }
90
+ }
91
+
92
+ return returnList
93
+ }
94
+
95
+ export const createLoopingList = <T >(list: T[], startIndex: number = 0, length: number = 0, forwards: boolean = true) => {
96
+ return createLoopingListWithIndex(list, startIndex, length, forwards).map(([_, item]) => item)
97
+ }
98
+
99
+ export const ArrayUtil = {
100
+ unique: <T>(list: T[]): T[] => {
101
+ const seen = new Set<T>()
102
+ return list.filter((item) => {
103
+ if (seen.has(item)) {
104
+ return false
105
+ }
106
+ seen.add(item)
107
+ return true
108
+ })
109
+ },
110
+
111
+ difference: <T>(list: T[], removeList: T[]): T[] => {
112
+ const remove = new Set<T>(removeList)
113
+ return list.filter((item) => !remove.has(item))
114
+ }
115
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A simple function that implements the builder pattern
3
+ * @param value The value to update which gets return with the same reference
4
+ * @param update The updates to apply on the object
5
+ */
6
+ export const builder = <T>(value: T, update: (value: T) => void): T => {
7
+ update(value)
8
+ return value
9
+ }
package/util/date.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { equalSizeGroups } from './array'
2
+
3
+ export const monthsList = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] as const
4
+ export type Month = typeof monthsList[number]
5
+
6
+ export const weekDayList = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const
7
+ export type WeekDay = typeof weekDayList[number]
8
+
9
+ export const formatDate = (date: Date) => {
10
+ const year = date.getFullYear().toString().padStart(4, '0')
11
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
12
+ const day = (date.getDate()).toString().padStart(2, '0')
13
+ return `${year}-${month}-${day}`
14
+ }
15
+
16
+ export const formatDateTime = (date: Date) => {
17
+ const dateString = formatDate(date)
18
+ const hours = date.getHours().toString().padStart(2, '0')
19
+ const minutes = date.getMinutes().toString().padStart(2, '0')
20
+ return `${dateString}T${hours}:${minutes}`
21
+ }
22
+
23
+ export const getDaysInMonth = (year: number, month: number): number => {
24
+ const lastDayOfMonth = new Date(year, month + 1, 0)
25
+ return lastDayOfMonth.getDate()
26
+ }
27
+
28
+ export type Duration = {
29
+ years?: number,
30
+ months?: number,
31
+ days?: number,
32
+ hours?: number,
33
+ minutes?: number,
34
+ seconds?: number,
35
+ milliseconds?: number,
36
+ }
37
+
38
+ export const changeDuration = (date: Date, duration: Duration, isAdding?: boolean): Date => {
39
+ const {
40
+ years = 0,
41
+ months = 0,
42
+ days = 0,
43
+ hours = 0,
44
+ minutes = 0,
45
+ seconds = 0,
46
+ milliseconds = 0,
47
+ } = duration
48
+
49
+ // Check ranges
50
+ if (years < 0) {
51
+ console.error(`Range error years must be greater than 0: received ${years}`)
52
+ return new Date(date)
53
+ }
54
+ if (months < 0 || months > 11) {
55
+ console.error(`Range error month must be 0 <= month <= 11: received ${months}`)
56
+ return new Date(date)
57
+ }
58
+ if (days < 0) {
59
+ console.error(`Range error days must be greater than 0: received ${days}`)
60
+ return new Date(date)
61
+ }
62
+ if (hours < 0 || hours > 23) {
63
+ console.error(`Range error hours must be 0 <= hours <= 23: received ${hours}`)
64
+ return new Date(date)
65
+ }
66
+ if (minutes < 0 || minutes > 59) {
67
+ console.error(`Range error minutes must be 0 <= minutes <= 59: received ${minutes}`)
68
+ return new Date(date)
69
+ }
70
+ if (seconds < 0 || seconds > 59) {
71
+ console.error(`Range error seconds must be 0 <= seconds <= 59: received ${seconds}`)
72
+ return new Date(date)
73
+ }
74
+ if (milliseconds < 0) {
75
+ console.error(`Range error seconds must be greater than 0: received ${milliseconds}`)
76
+ return new Date(date)
77
+ }
78
+
79
+ const multiplier = isAdding ? 1 : -1
80
+
81
+ const newDate = new Date(date)
82
+
83
+ newDate.setFullYear(newDate.getFullYear() + multiplier * years)
84
+
85
+ newDate.setMonth(newDate.getMonth() + multiplier * months)
86
+
87
+ newDate.setDate(newDate.getDate() + multiplier * days)
88
+
89
+ newDate.setHours(newDate.getHours() + multiplier * hours)
90
+
91
+ newDate.setMinutes(newDate.getMinutes() + multiplier * minutes)
92
+
93
+ newDate.setSeconds(newDate.getSeconds() + multiplier * seconds)
94
+
95
+ newDate.setMilliseconds(newDate.getMilliseconds() + multiplier * milliseconds)
96
+
97
+ return newDate
98
+ }
99
+
100
+ export const addDuration = (date: Date, duration: Duration): Date => {
101
+ return changeDuration(date, duration, true)
102
+ }
103
+
104
+ export const subtractDuration = (date: Date, duration: Duration): Date => {
105
+ return changeDuration(date, duration, false)
106
+ }
107
+
108
+ export const getBetweenDuration = (startDate: Date, endDate: Date): Duration => {
109
+ const durationInMilliseconds = endDate.getTime() - startDate.getTime()
110
+
111
+ const millisecondsInSecond = 1000
112
+ const millisecondsInMinute = 60 * millisecondsInSecond
113
+ const millisecondsInHour = 60 * millisecondsInMinute
114
+ const millisecondsInDay = 24 * millisecondsInHour
115
+ const millisecondsInMonth = 30 * millisecondsInDay // Rough estimation, can be adjusted
116
+
117
+ const years = Math.floor(durationInMilliseconds / (365.25 * millisecondsInDay))
118
+ const months = Math.floor(durationInMilliseconds / millisecondsInMonth)
119
+ const days = Math.floor(durationInMilliseconds / millisecondsInDay)
120
+ const hours = Math.floor((durationInMilliseconds % millisecondsInDay) / millisecondsInHour)
121
+ const seconds = Math.floor((durationInMilliseconds % millisecondsInHour) / millisecondsInSecond)
122
+ const milliseconds = durationInMilliseconds % millisecondsInSecond
123
+
124
+ return {
125
+ years,
126
+ months,
127
+ days,
128
+ hours,
129
+ seconds,
130
+ milliseconds,
131
+ }
132
+ }
133
+
134
+ /** Checks if a given date is in the range of two dates
135
+ *
136
+ * An undefined value for startDate or endDate means no bound for the start or end respectively
137
+ */
138
+ export const isInTimeSpan = (value: Date, startDate?: Date, endDate?: Date): boolean => {
139
+ if(startDate && endDate) {
140
+ console.assert(startDate <= endDate)
141
+ return startDate <= value && value <= endDate
142
+ } else if (startDate) {
143
+ return startDate <= value
144
+ } else if(endDate) {
145
+ return endDate >= value
146
+ } else {
147
+ return true
148
+ }
149
+ }
150
+
151
+ /** Compare two dates on the year, month, day */
152
+ export const equalDate = (date1: Date, date2: Date) => {
153
+ return date1.getFullYear() === date2.getFullYear()
154
+ && date1.getMonth() === date2.getMonth()
155
+ && date1.getDate() === date2.getDate()
156
+ }
157
+
158
+ export const getWeeksForCalenderMonth = (date: Date, weekStart: WeekDay, weeks: number = 6) => {
159
+ const month = date.getMonth()
160
+ const year = date.getFullYear()
161
+
162
+ const dayList: Date[] = []
163
+ let currentDate = new Date(year, month, 1) // Start of month
164
+ const weekStartIndex = weekDayList.indexOf(weekStart)
165
+
166
+ // Move the current day to the week before
167
+ while (currentDate.getDay() !== weekStartIndex) {
168
+ currentDate = subtractDuration(currentDate, { days: 1 })
169
+ }
170
+
171
+ while (dayList.length < 7 * weeks) {
172
+ const date = new Date(currentDate)
173
+ date.setHours(date.getHours(), date.getMinutes()) // To make sure we are not overwriting the time
174
+ dayList.push(date)
175
+ currentDate = addDuration(currentDate, { days: 1 })
176
+ }
177
+
178
+ // weeks
179
+ return equalSizeGroups(dayList, 7)
180
+ }
@@ -0,0 +1,37 @@
1
+ import { clamp } from './math'
2
+
3
+ export type EaseFunction = (t: number) => number
4
+
5
+ export class EaseFunctions {
6
+ static cubicBezierGeneric(x1: number, y1: number, x2: number, y2: number): { x: EaseFunction, y: EaseFunction } {
7
+ // Calculate the x and y coordinates using the cubic Bézier formula
8
+ const cx = 3 * x1
9
+ const bx = 3 * (x2 - x1) - cx
10
+ const ax = 1 - cx - bx
11
+
12
+ const cy = 3 * y1
13
+ const by = 3 * (y2 - y1) - cy
14
+ const ay = 1 - cy - by
15
+
16
+ // Compute x and y values at parameter t
17
+ const x = (t: number) => ((ax * t + bx) * t + cx) * t
18
+ const y = (t: number) => ((ay * t + by) * t + cy) * t
19
+
20
+ return {
21
+ x,
22
+ y
23
+ }
24
+ }
25
+
26
+ static cubicBezier(x1: number, y1: number, x2: number, y2: number): EaseFunction {
27
+ const { y } = EaseFunctions.cubicBezierGeneric(x1, y1, x2, y2)
28
+ return (t: number) => {
29
+ t = clamp(t)
30
+ return y(t) // <= equal to x(t) * 0 + y(t) * 1
31
+ }
32
+ }
33
+
34
+ static easeInEaseOut(t: number): number {
35
+ return EaseFunctions.cubicBezier(0.65, 0, 0.35, 1)(t)
36
+ };
37
+ }
@@ -0,0 +1,3 @@
1
+ export const validateEmail = (email: string): boolean => {
2
+ return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)
3
+ }