@codeleap/utils 4.3.0

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/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@codeleap/utils",
3
+ "version": "4.3.0",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/utils"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "4.3.0",
13
+ "@codeleap/types": "4.3.0",
14
+ "ts-node-dev": "1.1.8"
15
+ },
16
+ "scripts": {
17
+ "build": "echo 'No build needed'"
18
+ },
19
+ "peerDependencies": {
20
+ "@codeleap/types": "4.3.0",
21
+ "axios": "^1.7.9",
22
+ "typescript": "5.0.4",
23
+ "react": "18.1.0",
24
+ "@tanstack/react-query": "5.60.6"
25
+ },
26
+ "dependencies": {
27
+ "tinycolor2": "^1.4.2",
28
+ "deep-equal": "^2.0.5"
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@codeleap/utils",
3
+ "version": "4.3.0",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/utils"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "workspace:*",
13
+ "@codeleap/types": "workspace:*",
14
+ "ts-node-dev": "1.1.8"
15
+ },
16
+ "scripts": {
17
+ "build": "echo 'No build needed'"
18
+ },
19
+ "peerDependencies": {
20
+ "@codeleap/types": "workspace:*",
21
+ "axios": "^1.7.9",
22
+ "typescript": "5.0.4",
23
+ "react": "18.1.0",
24
+ "@tanstack/react-query": "5.60.6"
25
+ },
26
+ "dependencies": {
27
+ "tinycolor2": "^1.4.2",
28
+ "deep-equal": "^2.0.5"
29
+ }
30
+ }
package/src/array.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { FunctionType } from '@codeleap/types'
2
+
3
+ type GetterFunction<T> = FunctionType<[T, number], string | number> | keyof T
4
+
5
+ export function objectFromArray<T, Getter extends GetterFunction<T>>(
6
+ arr: T[],
7
+ keyAccessor?: Getter,
8
+ ): Record<string, T> {
9
+ let getObjectKey = (_, idx) => idx
10
+
11
+ if (keyAccessor) {
12
+ switch (typeof keyAccessor) {
13
+ case 'string':
14
+ getObjectKey = (value) => value[keyAccessor]
15
+ break
16
+ case 'function':
17
+ getObjectKey = keyAccessor
18
+ break
19
+ }
20
+ }
21
+
22
+ const indexedMap = arr.map((value, idx) => [getObjectKey(value, idx), value])
23
+
24
+ return Object.fromEntries(indexedMap)
25
+ }
26
+
27
+ export function uniqueArrayByProperty<T, G extends GetterFunction<T>>(
28
+ array: T[],
29
+ getProperty: G,
30
+ ) {
31
+ return Object.values(objectFromArray(array, getProperty))
32
+ }
33
+
34
+ export function flatten<T extends unknown>(arr: T[]) {
35
+ let newArr = [] as T[]
36
+
37
+ arr.forEach((item) => {
38
+ if (Array.isArray(item)) {
39
+ newArr = [...newArr, ...flatten(item)]
40
+ } else {
41
+ newArr.push(item)
42
+ }
43
+ })
44
+
45
+ return newArr
46
+ }
47
+
48
+ export function range(start: number, end: number) {
49
+ const length = end - start + 1
50
+ return Array.from({ length }, (_, index) => index + start)
51
+ }
@@ -0,0 +1,43 @@
1
+ export function cloneDeep(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return value
4
+ }
5
+
6
+ if (Array.isArray(value)) {
7
+ return value.map(cloneDeep)
8
+ }
9
+
10
+ if (value instanceof Date) {
11
+ return new Date(value.getTime())
12
+ }
13
+
14
+ if (value instanceof Map) {
15
+ const clonedMap = new Map()
16
+ value.forEach((v, k) => {
17
+ clonedMap.set(k, cloneDeep(v))
18
+ })
19
+ return clonedMap
20
+ }
21
+
22
+ if (value instanceof Set) {
23
+ const clonedSet = new Set()
24
+ value.forEach((v) => {
25
+ clonedSet.add(cloneDeep(v))
26
+ })
27
+ return clonedSet
28
+ }
29
+
30
+ if (value instanceof RegExp) {
31
+ return new RegExp(value.source, value.flags)
32
+ }
33
+
34
+ const clonedObj = {}
35
+
36
+ for (const key in value) {
37
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
38
+ clonedObj[key] = cloneDeep(value[key])
39
+ }
40
+ }
41
+
42
+ return clonedObj
43
+ }
package/src/colors.ts ADDED
@@ -0,0 +1,47 @@
1
+ import tinycolor from 'tinycolor2'
2
+ import { TypeGuards } from '@codeleap/types'
3
+
4
+ export function hexToRgb(hex) {
5
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
6
+ return result
7
+ ? {
8
+ r: parseInt(result[1], 16),
9
+ g: parseInt(result[2], 16),
10
+ b: parseInt(result[3], 16),
11
+ }
12
+ : null
13
+ }
14
+
15
+ const shadeColorCache = {}
16
+ export function shadeColor(color: string, percent = 0, opacity = null) {
17
+ const _color = color.trim()
18
+ const serialParams = [_color, percent.toString()]
19
+ if (TypeGuards.isNumber(opacity)) {
20
+ serialParams.push(opacity.toString())
21
+ }
22
+ const cacheKey = serialParams.join('/')
23
+
24
+ if (!!shadeColorCache[cacheKey]) {
25
+ return shadeColorCache[cacheKey]
26
+ }
27
+ const cl = tinycolor(_color)
28
+ if (percent !== 0) {
29
+
30
+ const shouldDarken = percent < 0
31
+
32
+ if (shouldDarken) {
33
+ cl.darken(-percent)
34
+ } else {
35
+ cl.lighten(percent)
36
+ }
37
+ }
38
+ if (TypeGuards.isNumber(opacity)) {
39
+ cl.setAlpha(opacity)
40
+
41
+ }
42
+
43
+ const rgbObj = cl.toRgb()
44
+ const rgbStr = `rgba(${rgbObj.r},${rgbObj.g},${rgbObj.b},${rgbObj.a})`
45
+ shadeColorCache[cacheKey] = rgbStr
46
+ return rgbStr
47
+ }
package/src/faker.ts ADDED
@@ -0,0 +1,34 @@
1
+ const names = [
2
+ 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas',
3
+ 'Charles', 'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Donald', 'Mark', 'Paul', 'Steven',
4
+ 'Andrew', 'Kenneth', 'Joshua', 'Kevin', 'Brian', 'George', 'Edward', 'Ronald', 'Timothy',
5
+ 'Jason', 'Jeffrey', 'Ryan', 'Gary', 'Jacob'
6
+ ]
7
+
8
+ const surnames = [
9
+ 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
10
+ 'Lopez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore',
11
+ 'Jackson', 'Martin', 'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark',
12
+ 'Ramirez', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott', 'Torres',
13
+ ]
14
+
15
+ const animals = [
16
+ 'lion', 'tiger', 'zebra', 'panda', 'koala', 'bear',
17
+ 'wolf', 'fox', 'rabbit', 'bat', 'spider', 'frog', 'shark'
18
+ ]
19
+
20
+ function getRandom(list: Array<string>) {
21
+ return list[Math.floor(Math.random() * list.length)]
22
+ }
23
+
24
+ function number(min: number = 0, max: number = 100) {
25
+ return Math.floor(Math.random() * (max - min + 1)) + min
26
+ }
27
+
28
+ export const faker = {
29
+ lastName: () => getRandom(surnames),
30
+ firstName: () => getRandom(names),
31
+ animal: () => getRandom(animals),
32
+ number,
33
+ name: () => `${getRandom(names)} ${getRandom(surnames)}`
34
+ }
package/src/file.ts ADDED
@@ -0,0 +1,23 @@
1
+ const separators = /[\\\/]+/
2
+
3
+ export function parseFilePathData(path: string) {
4
+ const parts = path.split(separators)
5
+
6
+ const lastPart = parts[parts.length - 1]
7
+
8
+ let fileName = lastPart
9
+ let ext = ''
10
+
11
+ if (lastPart.includes('.')) {
12
+ const dotIdx = fileName.lastIndexOf('.')
13
+ fileName = fileName.substring(0, dotIdx)
14
+
15
+ ext = lastPart.substring(dotIdx + 1)
16
+ }
17
+
18
+ return {
19
+ path: parts.slice(0, -1).join('/'),
20
+ extension: ext,
21
+ name: fileName,
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './array'
2
+ export * from './colors'
3
+ export * from './misc'
4
+ export * from './object'
5
+ export * from './react'
6
+ export * from './file'
7
+ export * from './string'
8
+ export * from './faker'
9
+ export * from './cloneDeep'
package/src/misc.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { capitalize } from './string'
2
+ import { StylesOf } from '@codeleap/types'
3
+
4
+ export function imagePathToFileObject(imagePath: string | null) {
5
+ const parts = imagePath ? imagePath.split('.') : ''
6
+
7
+ const ext = imagePath ? parts[parts.length - 1].toLowerCase() : ''
8
+
9
+ const fileValue = imagePath
10
+ ? {
11
+ uri: imagePath,
12
+ name: 'image_' + imagePath,
13
+ type: `image/${ext}`,
14
+ }
15
+ : null
16
+
17
+ return fileValue
18
+ }
19
+
20
+ const letterToColorMap = {
21
+ a: '#7CB9E8',
22
+ b: '#3a9e77',
23
+ c: '#A3C1AD',
24
+ d: '#E1BD27',
25
+ e: '#badc58',
26
+ f: '#db5970',
27
+ g: '#9b8ef1',
28
+ h: '#ffe169',
29
+ i: '#3ea9d1',
30
+ j: '#8aa341',
31
+ k: '#baf2f5',
32
+ l: '#ffa02d',
33
+ m: '#d46830',
34
+ n: '#62ecaa',
35
+ o: '#ffbe50',
36
+ p: '#0078D7',
37
+ q: '#8764B8',
38
+ r: '#52dd64',
39
+ s: '#7edce9',
40
+ t: '#dadd5d',
41
+ u: '#e9b55d',
42
+ v: '#99d669',
43
+ w: '#a3c83a',
44
+ x: '#f28d67',
45
+ y: '#ea82ec',
46
+ z: '#ff8295',
47
+ }
48
+
49
+ export function matchInitialToColor(anyString?: string) {
50
+ if (!anyString) return '#999999'
51
+ return letterToColorMap[anyString.toLowerCase().charAt(0)] || '#999999'
52
+ }
53
+
54
+ export function waitFor(ms) {
55
+ return new Promise<void>((resolve) => {
56
+ setTimeout(() => {
57
+ resolve()
58
+ }, ms)
59
+ })
60
+ }
61
+
62
+ type ParseSourceUrlArg = {
63
+ source?: string
64
+ src?: string
65
+ }
66
+
67
+ export function parseSourceUrl(args: string, Settings?: any): string
68
+ export function parseSourceUrl(
69
+ args: ParseSourceUrlArg,
70
+ Settings?: any
71
+ ): string
72
+ export function parseSourceUrl(
73
+ args: ParseSourceUrlArg | string,
74
+ Settings?: any,
75
+ ): string {
76
+ if (!args) return null
77
+
78
+ let res = ''
79
+ let address = ''
80
+ if (typeof args === 'string') {
81
+ address = args
82
+ } else {
83
+ address = args.source || args.src || ''
84
+ }
85
+
86
+ if (address && address.startsWith('/media/')) {
87
+ const tmp = address.substr(1, address.length)
88
+ res = `${Settings.BaseURL}${tmp}`
89
+ } else if (address) {
90
+ res = address
91
+ } else {
92
+ res = `https://picsum.photos/600?random=${Math.random() * 100}`
93
+ }
94
+ return res
95
+ }
96
+
97
+ export function getNestedStylesByKey<T extends StylesOf<any>>(match:string, variantStyles: T) {
98
+ const styles = {}
99
+
100
+ for (const [key, value] of Object.entries(variantStyles)) {
101
+
102
+ if (key.startsWith(match)) {
103
+ const partName = capitalize(key.replace(match, ''), true)
104
+ styles[partName] = value
105
+ }
106
+ }
107
+
108
+ return styles
109
+ }
110
+
111
+ export function hasFastRefreshed(Settings: any) {
112
+ if (Settings?.Environment?.InitTime) {
113
+ const timeFromStartup = (new Date()).getTime() - Settings.Environment.InitTime.getTime()
114
+ // It usually takes less than a seconds (~300ms) from app launch to running this, so if's been more than that we've probably fast refreshed
115
+ const fastRefreshed = Settings.Environment.IsDev && timeFromStartup > 1000
116
+ return fastRefreshed
117
+ } else {
118
+ console.log('hasFreshRefreshed() => Missing datetime from settings, please include to make this work')
119
+ return undefined
120
+ }
121
+ }
122
+
123
+ const throttleTimerId = []
124
+
125
+ export function throttle(func, ref, delay) {
126
+ if (throttleTimerId[ref]) {
127
+ return
128
+ }
129
+
130
+ throttleTimerId[ref] = setTimeout(function () {
131
+ func()
132
+ throttleTimerId[ref] = undefined
133
+ }, delay)
134
+ }
package/src/object.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { FunctionType, AppSettings } from '@codeleap/types'
2
+
3
+ export function deepMerge(base = {}, changes = {}): any {
4
+ const obj = {
5
+ ...base,
6
+ }
7
+ let changeEntries = []
8
+ try {
9
+ changeEntries = Object.entries(changes)
10
+ } catch (e) {
11
+ return changes
12
+ }
13
+
14
+ for (const [key, value] of changeEntries) {
15
+ obj[key] =
16
+ typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
17
+ ? deepMerge(obj[key], changes[key])
18
+ : value
19
+ }
20
+
21
+ return obj
22
+ }
23
+
24
+ export function mapObject<T>(
25
+ obj: T,
26
+ callback: FunctionType<[[keyof T, T[keyof T]]], any>,
27
+ ) {
28
+ return Object.entries(obj).map((args) => callback(args as [keyof T, T[keyof T]]),
29
+ )
30
+ }
31
+
32
+ export const deepSet = ([path, value]) => {
33
+ const parts = path.split('.')
34
+ const newObj = Array.isArray(value) ? [] : {}
35
+
36
+ if (parts.length === 1) {
37
+ newObj[parts[0]] = value
38
+ } else {
39
+ newObj[parts[0]] = deepSet([parts.slice(1).join('.'), value])
40
+ }
41
+
42
+ return newObj
43
+ }
44
+
45
+ export const deepGet = (path, obj) => {
46
+ const parts = path.split('.')
47
+ let newObj = { ...obj }
48
+
49
+ for (const prop of parts) {
50
+ newObj = newObj[prop]
51
+ }
52
+
53
+ return newObj
54
+ }
55
+
56
+ export function objectPaths(obj) {
57
+ let paths = []
58
+
59
+ Object.entries(obj).forEach(([key, value]) => {
60
+ if (!Array.isArray(value) && typeof value === 'object') {
61
+ paths = [...paths, ...objectPaths(value).map((k) => `${key}.${k}`)]
62
+ } else {
63
+ paths.push(key)
64
+ }
65
+ })
66
+
67
+ return paths
68
+ }
69
+
70
+ export function isValuePrimitive(a:any) {
71
+ return ['string', 'number', 'boolean'].includes(typeof a)
72
+ }
73
+
74
+ export function optionalObject(condition: boolean, ifTrue: any, ifFalse: any) {
75
+ return condition ? ifTrue : ifFalse
76
+ }
77
+
78
+ type TraverseRecArgs = {path:string[]; value: any; depth: number; key: string; type: string; primitive: boolean}
79
+
80
+ type TraverseCallback = (args?: TraverseRecArgs) => {stop?: boolean } | void
81
+
82
+ export function traverse(obj = {}, callback:TraverseCallback, args?: TraverseRecArgs) {
83
+ const isPrimitive = isValuePrimitive(obj)
84
+
85
+ const info = {
86
+ path: [],
87
+ depth: 0,
88
+ key: '',
89
+ type: typeof obj,
90
+ value: obj,
91
+ primitive: isPrimitive,
92
+ ...args,
93
+ }
94
+
95
+ if (isPrimitive) {
96
+ callback({
97
+ ...info,
98
+ depth: info.depth,
99
+
100
+ })
101
+ } else {
102
+ for (const [key, value] of Object.entries(obj || {})) {
103
+ const isPrimitive = isValuePrimitive(value)
104
+
105
+ if (!isPrimitive) {
106
+
107
+ callback({
108
+ ...info,
109
+ key,
110
+ value,
111
+ type: typeof value,
112
+ primitive: isPrimitive,
113
+ path: [...info.path, key],
114
+ })
115
+ }
116
+
117
+ traverse(value, callback, {
118
+ ...info,
119
+ key,
120
+ value,
121
+ type: typeof value,
122
+ primitive: isValuePrimitive(value),
123
+ path: [...info.path, key],
124
+ depth: info.depth + 1,
125
+ })
126
+ }
127
+ }
128
+ }
129
+
130
+ export function createSettings<T extends AppSettings>(a:T): T {
131
+ return a
132
+ }
133
+
134
+ export function extractKey(obj:any) {
135
+ if (obj.id) {
136
+ return obj.id
137
+ }
138
+ }
139
+
140
+ export function objectPickBy<K extends string = string, P = any>(obj: Record<K, P>, predicate: (valueKey: P, key: K) => boolean) {
141
+ const result = {} as Record<K, P>
142
+
143
+ for (const key in obj) {
144
+ if (obj?.hasOwnProperty?.(key) && predicate?.(obj?.[key], key)) {
145
+ result[key] = obj?.[key]
146
+ }
147
+ }
148
+
149
+ return result
150
+ }
151
+
152
+ export function transformObject<K extends string = string, T extends string = string>(obj: Record<K, T>, predicate: (value: T, key: K) => [K, T]): Record<string, any> {
153
+ const result = {}
154
+
155
+ for (const key in obj) {
156
+ const [newKey, newValue] = predicate?.(obj?.[key], key)
157
+
158
+ result[newKey as string] = newValue
159
+ }
160
+
161
+ return result
162
+ }
package/src/react.tsx ADDED
@@ -0,0 +1,107 @@
1
+ import equals from 'deep-equal'
2
+ import React from 'react'
3
+ import { TypeGuards } from '@codeleap/types'
4
+
5
+ export const deepEqual = equals
6
+
7
+ type ArePropsEqualOptions<MergedObject> = {
8
+ check: (keyof MergedObject)[]
9
+ excludeKeys?: string[]
10
+ }
11
+
12
+ export function arePropsEqual<A, B>(
13
+ previous: A,
14
+ next: B,
15
+ options: ArePropsEqualOptions<A & B>,
16
+ ) {
17
+ const { check, excludeKeys = [] } = options
18
+
19
+ for (const c of check) {
20
+ const nextItem = next[c as string]
21
+ const prevItem = previous[c as string]
22
+
23
+ for (const key of excludeKeys) {
24
+ if (nextItem?.[key]) delete nextItem[key]
25
+ if (prevItem?.[key]) delete prevItem[key]
26
+ }
27
+
28
+ const propsAreEqual = equals(nextItem, prevItem)
29
+
30
+ if (!propsAreEqual) {
31
+ return false
32
+ }
33
+ }
34
+
35
+ return true
36
+ }
37
+
38
+ export const flattenChildren = (children, flat = []) => {
39
+ flat = [...flat, ...React.Children.toArray(children)]
40
+
41
+ if (children.props && children.props.children) {
42
+ return flattenChildren(children.props.children, flat)
43
+ }
44
+
45
+ return flat
46
+ }
47
+
48
+ export const simplifyChildren = children => {
49
+ const flat = flattenChildren(children)
50
+
51
+ return flat.map(
52
+ ({
53
+ key,
54
+ ref,
55
+ type,
56
+ props: {
57
+ children,
58
+ ...props
59
+ },
60
+ }) => ({
61
+ key, ref, type, props,
62
+ }),
63
+ )
64
+ }
65
+
66
+ export function getRenderedComponent<P = any>(
67
+ ComponentOrProps: React.ComponentType<P> | P | React.ReactNode | null | undefined,
68
+ DefaultComponent: React.ComponentType<P>,
69
+ props?: P,
70
+ ): React.ReactNode {
71
+ if (TypeGuards.isNil(ComponentOrProps) || Object.keys(ComponentOrProps).length === 0) {
72
+ return null
73
+ }
74
+
75
+ if (TypeGuards.isFunction(ComponentOrProps)) {
76
+ return <ComponentOrProps {...props} />
77
+ }
78
+
79
+ if (React.isValidElement(ComponentOrProps)) {
80
+ return ComponentOrProps
81
+ }
82
+
83
+ const _props = ComponentOrProps as P
84
+
85
+ return <DefaultComponent {...props} {..._props} />
86
+ }
87
+
88
+ export function memoize<P extends object>(ComponentToMemoize: React.FunctionComponent<P>): React.NamedExoticComponent<P> {
89
+ return React.memo(ComponentToMemoize, () => true)
90
+ }
91
+
92
+ export function memoChecker<P>(prop: keyof P, prevProps: P, nextProps: P): boolean {
93
+ const nextItem = nextProps[prop]
94
+ const prevItem = prevProps[prop]
95
+
96
+ return equals(nextItem, prevItem)
97
+ }
98
+
99
+ export function memoBy<P extends object>(
100
+ ComponentToMemoize: React.FunctionComponent<P>,
101
+ check: keyof P | Array<keyof P>
102
+ ): React.NamedExoticComponent<P> {
103
+ return React.memo(ComponentToMemoize, (prevProps, nextProps) => {
104
+ const checks = Array.isArray(check) ? check : [check]
105
+ return checks.every((key) => memoChecker(key, prevProps, nextProps))
106
+ })
107
+ }
package/src/string.ts ADDED
@@ -0,0 +1,49 @@
1
+ export function singleLine(text: string) {
2
+ return text?.replace(/\n/g, ' ')
3
+ }
4
+
5
+ export function stringiparse(string) {
6
+ return JSON.parse(JSON.stringify(string))
7
+ }
8
+
9
+ export function capitalize(str: string, reverse = false) {
10
+ if (!str.length) return str
11
+ const firstChar = reverse ? str[0].toLowerCase() : str[0].toUpperCase()
12
+ return firstChar + str.substring(1)
13
+ }
14
+
15
+ export function isUppercase(char: string) {
16
+ return /[A-Z]|[\u0080-\u024F]/.test(char) && char.toUpperCase() === char
17
+ }
18
+
19
+ export function isLowercase(char: string) {
20
+ return !isUppercase(char)
21
+ }
22
+
23
+ export function humanizeCamelCase(str: string) {
24
+ const characters = []
25
+ let previousCharacter = ''
26
+ str.split('').forEach((char, idx) => {
27
+ if (idx === 0) {
28
+ characters.push(char.toUpperCase())
29
+ } else {
30
+ if (isUppercase(char) && isLowercase(previousCharacter)) {
31
+ characters.push(` ${char}`)
32
+ } else {
33
+ characters.push(char)
34
+ }
35
+ }
36
+
37
+ previousCharacter = char
38
+ })
39
+
40
+ return characters.join('')
41
+ }
42
+
43
+ export function ellipsis(str: string, maxLen: number) {
44
+ if (str.length - 3 > maxLen) {
45
+ return str.slice(0, maxLen - 3) + '...'
46
+ }
47
+
48
+ return str
49
+ }