@indico-data/design-system 1.0.52 → 1.0.53
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/jest.config.js +1 -2
- package/lib/components/Icon/Icon.stories.d.ts +2 -2
- package/lib/components/Icon/indicons.d.ts +1 -0
- package/lib/components/Navigation/Drawer/constants.d.ts +3 -0
- package/lib/components/index.d.ts +1 -1
- package/lib/components/inputs/DatePicker/DatePicker.d.ts +15 -0
- package/lib/components/inputs/DatePicker/DatePicker.stories.d.ts +6 -0
- package/lib/components/inputs/DatePicker/DatePicker.styles.d.ts +2 -0
- package/lib/components/inputs/DatePicker/index.d.ts +1 -0
- package/lib/components/inputs/NoInputDatePicker/NoInputDatePicker.d.ts +19 -0
- package/lib/components/inputs/NoInputDatePicker/NoInputDatePicker.stories.d.ts +7 -0
- package/lib/components/inputs/NoInputDatePicker/NoInputDatePicker.styles.d.ts +2 -0
- package/lib/components/inputs/NoInputDatePicker/__tests__/NoInputDatePicker.test.d.ts +1 -0
- package/lib/components/inputs/NoInputDatePicker/index.d.ts +1 -0
- package/lib/components/inputs/index.d.ts +2 -0
- package/lib/components/text-truncate/TextTruncate.d.ts +1 -1
- package/lib/index.d.ts +61 -28
- package/lib/index.esm.js +21441 -13325
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +21462 -13328
- package/lib/index.js.map +1 -1
- package/lib/types.d.ts +1 -0
- package/lib/utils/color.d.ts +21 -0
- package/lib/utils/index.d.ts +4 -0
- package/lib/utils/number.d.ts +21 -0
- package/lib/utils/string.d.ts +12 -0
- package/package.json +10 -3
- package/src/components/Icon/indicons.tsx +6 -0
- package/src/components/ListTable/Header/Header.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/basic-section/SectionTable/SectionTable.styles.ts +1 -2
- package/src/components/basic-section/SectionTable/SectionTable.tsx +12 -10
- package/src/components/dropdowns/MultiCombobox/MultiCombobox.stories.tsx +1 -1
- package/src/components/dropdowns/MultiCombobox/MultiCombobox.tsx +1 -1
- package/src/components/dropdowns/SingleCombobox/SingleCombobox.tsx +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/inputs/DatePicker/DatePicker.stories.tsx +30 -0
- package/src/components/inputs/DatePicker/DatePicker.styles.ts +437 -0
- package/src/components/inputs/DatePicker/DatePicker.tsx +165 -0
- package/src/components/inputs/DatePicker/index.ts +1 -0
- package/src/components/inputs/EditableInput/EditableInput.tsx +1 -1
- package/src/components/inputs/NoInputDatePicker/NoInputDatePicker.stories.tsx +52 -0
- package/src/components/inputs/NoInputDatePicker/NoInputDatePicker.styles.ts +449 -0
- package/src/components/inputs/NoInputDatePicker/NoInputDatePicker.tsx +204 -0
- package/src/components/inputs/NoInputDatePicker/__tests__/NoInputDatePicker.test.tsx +126 -0
- package/src/components/inputs/NoInputDatePicker/index.ts +1 -0
- package/src/components/inputs/TextInput/TextInput.stories.tsx +1 -1
- package/src/components/inputs/index.ts +2 -0
- package/src/components/modals/ModalBase/ModalBase.styles.tsx +1 -1
- package/src/components/text-truncate/TextTruncate.test.tsx +3 -4
- package/src/components/text-truncate/TextTruncate.tsx +3 -2
- package/src/index.ts +2 -0
- package/src/styles/globals/forms.ts +1 -9
- package/src/styles/globals/tables.ts +1 -5
- package/src/utils/color.ts +139 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/number.ts +29 -0
- package/src/utils/string.ts +87 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { NoInputDatePicker } from '../NoInputDatePicker';
|
|
4
|
+
import { DateRange } from 'react-day-picker';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
|
|
7
|
+
const today = new Date();
|
|
8
|
+
const mockOnChange = jest.fn();
|
|
9
|
+
let trigger: any;
|
|
10
|
+
|
|
11
|
+
describe('NoInputDatePicker', () => {
|
|
12
|
+
describe('single', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
render(
|
|
15
|
+
<NoInputDatePicker
|
|
16
|
+
id={'date-picker'}
|
|
17
|
+
label={'Pick a date'}
|
|
18
|
+
value={today}
|
|
19
|
+
isRangePicker={false}
|
|
20
|
+
triggerIcon="calendar"
|
|
21
|
+
triggerIconSize={[20]}
|
|
22
|
+
onChange={(date: Date | DateRange | undefined) => {
|
|
23
|
+
mockOnChange(date);
|
|
24
|
+
}}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
trigger = screen.getByTestId('datepicker-trigger-for-date-picker');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('shows single date picker', async () => {
|
|
32
|
+
await userEvent.click(trigger);
|
|
33
|
+
expect(screen.getByTestId('datepicker-dialog')).toBeVisible();
|
|
34
|
+
expect(screen.getByTestId('single-datepicker')).toBeVisible();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('opens the date picker when clicking on the trigger', async () => {
|
|
38
|
+
await userEvent.click(trigger);
|
|
39
|
+
const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
|
|
40
|
+
expect(datePickerDialogContainer).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('emits the right date picked for single', async () => {
|
|
44
|
+
await userEvent.click(trigger);
|
|
45
|
+
const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
|
|
46
|
+
expect(datePickerDialogContainer).toBeVisible();
|
|
47
|
+
|
|
48
|
+
const day28 = screen.getByText('28');
|
|
49
|
+
await userEvent.click(day28);
|
|
50
|
+
expect(mockOnChange).toHaveBeenCalledWith(
|
|
51
|
+
new Date(today.getFullYear(), today.getMonth(), 28),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('closes date picker on single selection', async () => {
|
|
56
|
+
await userEvent.click(trigger);
|
|
57
|
+
const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
|
|
58
|
+
expect(datePickerDialogContainer).toBeVisible();
|
|
59
|
+
|
|
60
|
+
const day28 = screen.getByText('28');
|
|
61
|
+
await userEvent.click(day28);
|
|
62
|
+
expect(mockOnChange).toHaveBeenCalledWith(
|
|
63
|
+
new Date(today.getFullYear(), today.getMonth(), 28),
|
|
64
|
+
);
|
|
65
|
+
expect(datePickerDialogContainer).not.toBeVisible();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('range', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
render(
|
|
72
|
+
<NoInputDatePicker
|
|
73
|
+
id={'date-picker'}
|
|
74
|
+
label={'Pick a date'}
|
|
75
|
+
value={today}
|
|
76
|
+
isRangePicker={true}
|
|
77
|
+
triggerIcon="calendar"
|
|
78
|
+
triggerIconSize={[20]}
|
|
79
|
+
onChange={(date: Date | DateRange | undefined) => {
|
|
80
|
+
mockOnChange(date);
|
|
81
|
+
}}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
trigger = screen.getByTestId('datepicker-trigger-for-date-picker');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('shows ranged date picker', async () => {
|
|
89
|
+
await userEvent.click(trigger);
|
|
90
|
+
expect(screen.getByTestId('datepicker-dialog')).toBeVisible();
|
|
91
|
+
expect(screen.getByTestId('range-datepicker')).toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('emits the right date picked for range', async () => {
|
|
95
|
+
await userEvent.click(trigger);
|
|
96
|
+
const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
|
|
97
|
+
expect(datePickerDialogContainer).toBeVisible();
|
|
98
|
+
|
|
99
|
+
const day27 = screen.getByText('27');
|
|
100
|
+
await userEvent.click(day27);
|
|
101
|
+
const day28 = screen.getByText('28');
|
|
102
|
+
await userEvent.click(day28);
|
|
103
|
+
|
|
104
|
+
expect(mockOnChange).toHaveBeenCalledWith({
|
|
105
|
+
from: new Date(today.getFullYear(), today.getMonth(), 27),
|
|
106
|
+
to: new Date(today.getFullYear(), today.getMonth(), 28),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it('closes date picker on both ranged selection', async () => {
|
|
110
|
+
await userEvent.click(trigger);
|
|
111
|
+
const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
|
|
112
|
+
expect(datePickerDialogContainer).toBeVisible();
|
|
113
|
+
|
|
114
|
+
const day27 = screen.getByText('27');
|
|
115
|
+
await userEvent.click(day27);
|
|
116
|
+
const day28 = screen.getByText('28');
|
|
117
|
+
await userEvent.click(day28);
|
|
118
|
+
|
|
119
|
+
expect(mockOnChange).toHaveBeenCalledWith({
|
|
120
|
+
from: new Date(today.getFullYear(), today.getMonth(), 27),
|
|
121
|
+
to: new Date(today.getFullYear(), today.getMonth(), 28),
|
|
122
|
+
});
|
|
123
|
+
expect(datePickerDialogContainer).not.toBeVisible();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NoInputDatePicker } from './NoInputDatePicker';
|
|
@@ -10,7 +10,7 @@ const validationErrors = [
|
|
|
10
10
|
'Another error that may not be so helpful',
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
function StoryRender(props) {
|
|
13
|
+
function StoryRender(props: any) {
|
|
14
14
|
const [value, setValue] = useState<string>(props.value);
|
|
15
15
|
|
|
16
16
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
@@ -4,3 +4,5 @@ export { SearchInput } from './SearchInput';
|
|
|
4
4
|
export { TextInput } from './TextInput';
|
|
5
5
|
export { Radio, RadioGroup } from './RadioButtons';
|
|
6
6
|
export { AbstractRadio, AbstractRadioGroup } from './RadioGroup';
|
|
7
|
+
export { DatePicker } from './DatePicker';
|
|
8
|
+
export { NoInputDatePicker } from './NoInputDatePicker';
|
|
@@ -3,7 +3,7 @@ import ReactModal from 'react-modal';
|
|
|
3
3
|
import styled from 'styled-components';
|
|
4
4
|
|
|
5
5
|
import { COLORS } from '@/tokens';
|
|
6
|
-
import { colorUtils } from '
|
|
6
|
+
import { colorUtils } from '@/utils';
|
|
7
7
|
|
|
8
8
|
// see: https://github.com/reactjs/react-modal/issues/603
|
|
9
9
|
function ModalAdapter(props: any): React.ReactElement {
|
|
@@ -6,13 +6,12 @@ describe('TextTruncate', () => {
|
|
|
6
6
|
const longText = 'this is a really long string that should be truncated';
|
|
7
7
|
|
|
8
8
|
it('truncates text when it exceeds the maximum length and adds a title attr', () => {
|
|
9
|
-
const { getByTitle } = render(<TextTruncate string={longText} maxChars={20}
|
|
9
|
+
const { getByTitle } = render(<TextTruncate string={longText} maxChars={20}></TextTruncate>);
|
|
10
10
|
expect(getByTitle(longText).textContent).toEqual('this is a really lon...');
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('does not truncate the text when it is below the max length', () => {
|
|
14
|
-
const { getByText } = render(<TextTruncate string={longText} maxChars={200}
|
|
14
|
+
const { getByText } = render(<TextTruncate string={longText} maxChars={200}></TextTruncate>);
|
|
15
15
|
expect(getByText(longText).textContent).toEqual(longText);
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
});
|
|
17
|
+
});
|
|
@@ -4,7 +4,7 @@ import { StyledTextTruncate } from './TextTruncate.styles';
|
|
|
4
4
|
interface TextTruncateProps {
|
|
5
5
|
string: string;
|
|
6
6
|
maxChars: number;
|
|
7
|
-
children
|
|
7
|
+
children?: any;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function TextTruncate({ string, maxChars, children }: TextTruncateProps) {
|
|
@@ -15,7 +15,8 @@ export function TextTruncate({ string, maxChars, children }: TextTruncateProps)
|
|
|
15
15
|
</StyledTextTruncate>
|
|
16
16
|
) : (
|
|
17
17
|
<StyledTextTruncate>
|
|
18
|
-
{string}
|
|
18
|
+
{string}
|
|
19
|
+
{children}
|
|
19
20
|
</StyledTextTruncate>
|
|
20
21
|
);
|
|
21
22
|
}
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
CircleSpinner,
|
|
27
27
|
ConfirmModal,
|
|
28
28
|
Drawer,
|
|
29
|
+
DatePicker,
|
|
29
30
|
EditableInput,
|
|
30
31
|
Icon,
|
|
31
32
|
IconButton,
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
LoadingList,
|
|
35
36
|
ModalBase,
|
|
36
37
|
MultiCombobox,
|
|
38
|
+
NoInputDatePicker,
|
|
37
39
|
NumberInput,
|
|
38
40
|
Pagination,
|
|
39
41
|
PercentageRing,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createGlobalStyle } from 'styled-components';
|
|
2
2
|
|
|
3
3
|
import { COLORS, TYPOGRAPHY, ANIMATION } from '@/tokens';
|
|
4
|
-
import { colorUtils } from '@indico-data/utils';
|
|
5
4
|
|
|
6
5
|
const formBoxShadow = 'inset 0 0 0 rgba(#000, 0)';
|
|
7
6
|
const formBoxShadowFocus = formBoxShadow;
|
|
@@ -59,10 +58,6 @@ export const Forms = createGlobalStyle`
|
|
|
59
58
|
transition: border-color ${ANIMATION.duration} ${ANIMATION.timing};
|
|
60
59
|
width: 100%;
|
|
61
60
|
|
|
62
|
-
&:hover {
|
|
63
|
-
// border-color: ${colorUtils.shade(COLORS.borderColor, 20)};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
61
|
&:focus {
|
|
67
62
|
border-color: ${COLORS.actionColor};
|
|
68
63
|
box-shadow: ${formBoxShadowFocus};
|
|
@@ -70,7 +65,6 @@ export const Forms = createGlobalStyle`
|
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
&:disabled {
|
|
73
|
-
// background-color: ${colorUtils.shade(COLORS.backgroundColor, 5)};
|
|
74
68
|
cursor: not-allowed;
|
|
75
69
|
|
|
76
70
|
&:hover {
|
|
@@ -78,9 +72,7 @@ export const Forms = createGlobalStyle`
|
|
|
78
72
|
}
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
|
|
82
|
-
// color: ${colorUtils.tint(COLORS.defaultFontColor, 40)};
|
|
83
|
-
}
|
|
75
|
+
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
textarea {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createGlobalStyle } from 'styled-components';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { COLORS, TYPOGRAPHY } from '@/tokens';
|
|
3
|
+
import { TYPOGRAPHY } from '@/tokens';
|
|
6
4
|
|
|
7
5
|
export const Tables = createGlobalStyle`
|
|
8
6
|
table {
|
|
@@ -14,14 +12,12 @@ export const Tables = createGlobalStyle`
|
|
|
14
12
|
|
|
15
13
|
th {
|
|
16
14
|
font-size: ${TYPOGRAPHY.fontSize.subheadSmall};
|
|
17
|
-
// border-bottom: 0.5px solid ${colorUtils.hexToRgb(COLORS.defaultFontColor, 0.1)};
|
|
18
15
|
padding: 20px 15px 5px;
|
|
19
16
|
font-weight: 400;
|
|
20
17
|
text-align: left;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
td {
|
|
24
|
-
// border-bottom: 0.5px solid ${colorUtils.hexToRgb(COLORS.defaultFontColor, 0.1)};
|
|
25
21
|
padding: 20px 15px;
|
|
26
22
|
font-size: ${TYPOGRAPHY.fontSize.base};
|
|
27
23
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export const mix = function (color_1: string, color_2: string, weight: number): string {
|
|
2
|
+
function d2h(d: number) {
|
|
3
|
+
return d.toString(16);
|
|
4
|
+
} // convert a decimal value to hex
|
|
5
|
+
function h2d(h: string) {
|
|
6
|
+
return parseInt(h, 16);
|
|
7
|
+
} // convert a hex value to decimal
|
|
8
|
+
function s2l(hex: string) {
|
|
9
|
+
return `${hex}${hex}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
weight = typeof weight !== 'undefined' ? weight : 50; // set the weight to 50%, if that argument is omitted
|
|
13
|
+
color_1 = color_1.replace(/#/g, '');
|
|
14
|
+
color_2 = color_2.replace(/#/g, '');
|
|
15
|
+
|
|
16
|
+
// check if colors are shorthand or longhand - covert to longhand
|
|
17
|
+
color_1 = color_1.length === 3 ? s2l(color_1) : color_1;
|
|
18
|
+
color_2 = color_2.length === 3 ? s2l(color_2) : color_2;
|
|
19
|
+
|
|
20
|
+
let color = '#';
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i <= 5; i += 2) {
|
|
23
|
+
// loop through each of the 3 hex pairs—red, green, and blue
|
|
24
|
+
const v1 = h2d(color_1.substr(i, 2)); // extract the current pairs
|
|
25
|
+
const v2 = h2d(color_2.substr(i, 2));
|
|
26
|
+
// combine the current pairs from each source color, according to the specified weight
|
|
27
|
+
let val = d2h(Math.floor(v2 + (v1 - v2) * (weight / 100.0)));
|
|
28
|
+
|
|
29
|
+
while (val.length < 2) {
|
|
30
|
+
val = '0' + val;
|
|
31
|
+
} // prepend a '0' if val results in a single digit
|
|
32
|
+
|
|
33
|
+
color += val; // concatenate val to our new color string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return color; // PROFIT!
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const shade = (color: string, percentage: number): string => {
|
|
40
|
+
return mix('#000000', color, percentage);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const tint = (color: string, percentage: number): string => {
|
|
44
|
+
return mix('#ffffff', color, percentage);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Converts hex color values to rgb or rgba values if a opacity is supplied
|
|
49
|
+
* @param hex
|
|
50
|
+
* @returns {*}
|
|
51
|
+
*/
|
|
52
|
+
export const hexToRgb = (hex: string, opacity?: number): string | undefined => {
|
|
53
|
+
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
54
|
+
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
55
|
+
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
|
|
56
|
+
return r + r + g + g + b + b;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
60
|
+
if (result && opacity) {
|
|
61
|
+
return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
|
62
|
+
result[3],
|
|
63
|
+
16,
|
|
64
|
+
)}, ${opacity})`;
|
|
65
|
+
} else if (result) {
|
|
66
|
+
return `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
|
67
|
+
result[3],
|
|
68
|
+
16,
|
|
69
|
+
)})`;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Takes a 3- or 6-character hex color value, and returns an object containing
|
|
76
|
+
* its equivalent HSL values.
|
|
77
|
+
*
|
|
78
|
+
* @see {@link https://css-tricks.com/converting-color-spaces-in-javascript/}
|
|
79
|
+
* @see {@link https://gist.github.com/mjackson/5311256}
|
|
80
|
+
*/
|
|
81
|
+
export function hexToHsl(hexColor: string): { hue: number; saturation: number; lightness: number } {
|
|
82
|
+
// convert to 6-character hex string, if necessary
|
|
83
|
+
const fullHexColor = (() => {
|
|
84
|
+
if (hexColor.length === 4) {
|
|
85
|
+
return (
|
|
86
|
+
'#' +
|
|
87
|
+
hexColor
|
|
88
|
+
.replace('#', '')
|
|
89
|
+
.split('')
|
|
90
|
+
.reduce((acc, char) => (acc += char + char), '')
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return hexColor;
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
const rgbValues = {
|
|
98
|
+
red: parseInt(fullHexColor.slice(1, 3), 16) / 255,
|
|
99
|
+
green: parseInt(fullHexColor.slice(3, 5), 16) / 255,
|
|
100
|
+
blue: parseInt(fullHexColor.slice(-2), 16) / 255,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const allValues = Object.values(rgbValues);
|
|
104
|
+
|
|
105
|
+
const channelMax = Math.max(...allValues);
|
|
106
|
+
const channelMin = Math.min(...allValues);
|
|
107
|
+
const channelDelta = channelMax - channelMin;
|
|
108
|
+
|
|
109
|
+
const hslValues = {
|
|
110
|
+
hue: 0,
|
|
111
|
+
saturation: 0,
|
|
112
|
+
lightness: (channelMax + channelMin) / 2,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (channelDelta !== 0) {
|
|
116
|
+
// calculate hue
|
|
117
|
+
if (channelMax === rgbValues.red) {
|
|
118
|
+
hslValues.hue = ((rgbValues.green - rgbValues.blue) / channelDelta) % 6;
|
|
119
|
+
} else if (channelMax === rgbValues.green) {
|
|
120
|
+
hslValues.hue = (rgbValues.blue - rgbValues.red) / channelDelta + 2;
|
|
121
|
+
} else {
|
|
122
|
+
hslValues.hue = (rgbValues.red - rgbValues.green) / channelDelta + 4;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// calculate saturation
|
|
126
|
+
hslValues.saturation = channelDelta / (1 - Math.abs(2 * hslValues.lightness - 1));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
hslValues.hue = Math.round(hslValues.hue * 60);
|
|
130
|
+
hslValues.saturation = +(hslValues.saturation * 100).toFixed(0);
|
|
131
|
+
hslValues.lightness = +(hslValues.lightness * 100).toFixed(0);
|
|
132
|
+
|
|
133
|
+
// make negative hues positive
|
|
134
|
+
if (hslValues.hue < 0) {
|
|
135
|
+
hslValues.hue += 360;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return hslValues;
|
|
139
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Takes a number and formats it nicely.
|
|
3
|
+
*
|
|
4
|
+
* @example numberWithCommas(12345)
|
|
5
|
+
* // returns 12,345
|
|
6
|
+
*/
|
|
7
|
+
export function numberWithCommas(value: number): string {
|
|
8
|
+
return new Intl.NumberFormat(navigator.language || 'en-US').format(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Takes a number and returns it, rounded up to the maximum decimal places specified.
|
|
13
|
+
*
|
|
14
|
+
* @example maxDecimalPlaces(0.1694915254237288, 2)
|
|
15
|
+
* // returns 0.17
|
|
16
|
+
*
|
|
17
|
+
* @example maxDecimalPlaces(0.1694915254237288, 3)
|
|
18
|
+
* // returns 0.169
|
|
19
|
+
*
|
|
20
|
+
* @example maxDecimalPlaces(12.902, 2)
|
|
21
|
+
* // returns 12.9
|
|
22
|
+
*/
|
|
23
|
+
export function maxDecimalPlaces(value: number, decimalPlaces: number): number {
|
|
24
|
+
return +value.toFixed(decimalPlaces);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatConfidence(confidence: number): number {
|
|
28
|
+
return Math.min(Math.floor(confidence * 100), 99);
|
|
29
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const camelCaseToUpperUnderscore = (string: string) => {
|
|
2
|
+
return string
|
|
3
|
+
.replace(/([A-Z])/g, function ($1) {
|
|
4
|
+
return '_' + $1;
|
|
5
|
+
})
|
|
6
|
+
.toUpperCase();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const camelCaseToSpaceUpper = (string: string) => {
|
|
10
|
+
return (
|
|
11
|
+
string
|
|
12
|
+
// insert a space before all caps
|
|
13
|
+
.replace(/([A-Z])/g, ' $1')
|
|
14
|
+
// uppercase the first character
|
|
15
|
+
.replace(/^./, function (str) {
|
|
16
|
+
return str.toUpperCase();
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const underscoreToCapitalize = (string: string) => {
|
|
22
|
+
return (
|
|
23
|
+
string
|
|
24
|
+
// replace all _ with spaces
|
|
25
|
+
.replace(/_/g, ' ')
|
|
26
|
+
// uppercase the first character
|
|
27
|
+
.replace(/^./, function (str) {
|
|
28
|
+
return str.toUpperCase();
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const snakeCaseToCamelCase = (string: string) => {
|
|
34
|
+
const parts = string.split('_').map((part, i) => {
|
|
35
|
+
if (i > 0) {
|
|
36
|
+
return part.replace(/^./, function (str) {
|
|
37
|
+
return str.toUpperCase();
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
return part;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return parts.join().replace(/,/g, '');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function capitalize(string: string): string {
|
|
48
|
+
if (typeof string !== 'string') return '';
|
|
49
|
+
|
|
50
|
+
return string.slice(0, 1).toUpperCase() + string.slice(1).toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function capitalizeFirstOnly(string: string): string {
|
|
54
|
+
if (typeof string !== 'string') return '';
|
|
55
|
+
|
|
56
|
+
return string.slice(0, 1).toUpperCase() + string.slice(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generates a random string of English alphabet characters. Defaults to a length of 8.
|
|
61
|
+
*/
|
|
62
|
+
export function createRandomString(length?: number): string {
|
|
63
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
64
|
+
let result = '';
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < (length || 8); i++) {
|
|
67
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const maxLengthWithEllipse = (string: string, maxLength: number) => {
|
|
74
|
+
if (string.length <= maxLength) {
|
|
75
|
+
return string;
|
|
76
|
+
} else {
|
|
77
|
+
return string.slice(0, maxLength - 2).concat('...');
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const maxLengthWithMiddleEllipsis = (string: string, maxLength: number): string => {
|
|
82
|
+
const frontSegment = string.slice(0, Math.floor(maxLength / 2) - 2);
|
|
83
|
+
const endSegmentLength = string.length - (Math.floor(maxLength / 2) - 1);
|
|
84
|
+
const endSegment = string.slice(endSegmentLength, string.length);
|
|
85
|
+
|
|
86
|
+
return string.length <= maxLength ? string : `${frontSegment}...${endSegment}`;
|
|
87
|
+
};
|