@vanilla-bean/components 1.0.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/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
package/utils/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Utils
|
|
2
|
+
|
|
3
|
+
General-purpose utility functions shared across components. All are available as named exports from the top-level package.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { debounce, orderBy, toCamelCase } from '@vanilla-bean/components';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## String
|
|
10
|
+
|
|
11
|
+
### `capitalize(string, recursive?, split?)`
|
|
12
|
+
|
|
13
|
+
Capitalizes the first letter of each word. Set `recursive` to also capitalize after digits. `split` defaults to a space.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
capitalize('hello world'); // → 'Hello World'
|
|
17
|
+
capitalize('step1thing', true); // → 'Step1Thing'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### `toCamelCase(string, splitter?)` / `fromCamelCase(string, joiner?)`
|
|
21
|
+
|
|
22
|
+
Round-trip between prose and camelCase.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
toCamelCase('component options'); // → 'componentOptions'
|
|
26
|
+
fromCamelCase('componentOptions'); // → 'component options'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `toPascalCase(string)`
|
|
30
|
+
|
|
31
|
+
Like `toCamelCase` but capitalizes the first word too. Useful for generated class names.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
toPascalCase('my component'); // → 'MyComponent'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `removeExcessIndentation(string)`
|
|
38
|
+
|
|
39
|
+
Strips the minimum common leading whitespace from every line. Useful for cleaning up indented template literals before rendering.
|
|
40
|
+
|
|
41
|
+
## Data
|
|
42
|
+
|
|
43
|
+
### `orderBy(orders)`
|
|
44
|
+
|
|
45
|
+
Returns an `Array.sort` comparator. `orders` is a single `{ property, direction }` object or an array of them for multi-key sorting.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
items.sort(orderBy({ property: 'name', direction: 'asc' }));
|
|
49
|
+
items.sort(
|
|
50
|
+
orderBy([
|
|
51
|
+
{ property: 'group', direction: 'asc' },
|
|
52
|
+
{ property: 'name', direction: 'asc' },
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `debounce(callback, delay?)` / `throttle(callback, delay?)`
|
|
58
|
+
|
|
59
|
+
Standard debounce and throttle. Default delay is 400ms.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const save = debounce(value => api.save(value), 300);
|
|
63
|
+
const onScroll = throttle(() => updatePosition(), 100);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `delay(ms)`
|
|
67
|
+
|
|
68
|
+
A promise that resolves after `ms` milliseconds.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
await delay(500); // pause for 500ms
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `retry(callback, options?)`
|
|
75
|
+
|
|
76
|
+
Retries an async function on failure. Options: `{ attempts, interval }`.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const data = await retry(() => fetch('/api/data'), { attempts: 3, interval: 1000 });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `convertRange(value, sourceRange, targetRange)`
|
|
83
|
+
|
|
84
|
+
Maps a number from one numeric range to another.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
convertRange(0.5, [0, 1], [0, 100]); // → 50
|
|
88
|
+
convertRange(128, [0, 255], [0, 1]); // → 0.502
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `conditionalList(items)`
|
|
92
|
+
|
|
93
|
+
Builds a flat array from `{ if, thenItem, alwaysItem }` entries, keeping only those where `if` is truthy. Used internally for conditional component content.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
conditionalList([
|
|
97
|
+
{ alwaysItem: 'home' },
|
|
98
|
+
{ if: isLoggedIn, thenItem: 'dashboard' },
|
|
99
|
+
{ if: isAdmin, thenItem: 'settings' },
|
|
100
|
+
]); // → ['home', 'dashboard'] when isLoggedIn, not isAdmin
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `getCustomProperties(object)`
|
|
104
|
+
|
|
105
|
+
Returns all own enumerable properties of an object, excluding prototype members.
|
|
106
|
+
|
|
107
|
+
## Random
|
|
108
|
+
|
|
109
|
+
### `rand(min?, max?)` / `randInt(min?, max?)` / `randFromArray(array)`
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
rand(0, 1); // float in [0, 1)
|
|
113
|
+
randInt(1, 6); // integer in [1, 6]
|
|
114
|
+
randFromArray(['a', 'b', 'c']); // random element
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Color
|
|
118
|
+
|
|
119
|
+
### `stringToColor(string, config?)`
|
|
120
|
+
|
|
121
|
+
Deterministic color from any string. Same input always produces the same hue. Useful for avatars or tag coloring.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
stringToColor('alice'); // always the same hue for 'alice'
|
|
125
|
+
stringToColor('bob'); // different but stable hue
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `rgbDelta(rgbA, rgbB)`
|
|
129
|
+
|
|
130
|
+
Perceptual distance between two RGB arrays `[r, g, b]`. Used internally by ColorPicker to score color accuracy in ShapeMatchGame.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
rgbDelta([255, 0, 0], [200, 0, 0]); // → ~55
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Element
|
|
137
|
+
|
|
138
|
+
### `isDescendantOf(element, parentElement)`
|
|
139
|
+
|
|
140
|
+
Whether `element` is anywhere inside `parentElement` in the DOM tree.
|
|
141
|
+
|
|
142
|
+
### `getElementsContainingText(text, options?)`
|
|
143
|
+
|
|
144
|
+
Returns all DOM elements whose visible text content matches `text`. Useful for testing and demo navigation.
|
|
145
|
+
|
|
146
|
+
## Browser
|
|
147
|
+
|
|
148
|
+
### `copyToClipboard(text)` / `readClipboard()`
|
|
149
|
+
|
|
150
|
+
Clipboard read/write. `copyToClipboard` returns `true` on success.
|
|
151
|
+
|
|
152
|
+
### `vibrate(durationPattern?)` / `tactileResponse()`
|
|
153
|
+
|
|
154
|
+
Haptic feedback on mobile. `tactileResponse` is a 30ms pulse shorthand.
|
|
155
|
+
|
|
156
|
+
### `isMac()`
|
|
157
|
+
|
|
158
|
+
Whether the current user agent is macOS. Useful for displaying ⌘ vs Ctrl keybindings.
|
|
159
|
+
|
|
160
|
+
### `isDev`
|
|
161
|
+
|
|
162
|
+
`true` outside production bundles. Guards dev-only warnings.
|
|
163
|
+
|
|
164
|
+
## Class
|
|
165
|
+
|
|
166
|
+
### `buildClassList(...classNames)` / `buildClassName(...classNames)`
|
|
167
|
+
|
|
168
|
+
Flatten nested arrays, filter falsy values, and produce a class name list or space-joined string. Used internally by `addClass`.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
buildClassName('btn', isActive && 'active', null, ['size-lg']); // → 'btn active size-lg'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `classSafeNanoid(size?)`
|
|
175
|
+
|
|
176
|
+
Generates a nanoid that is safe to use as a CSS class name: no leading digits, no special characters.
|
package/utils/browser.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the current runtime is in development mode.
|
|
3
|
+
* Checks both process.env.NODE_ENV (Node/Bun/webpack) and import.meta.env (Vite/ESBuild).
|
|
4
|
+
* Defaults to false (production-safe) when neither is set.
|
|
5
|
+
*/
|
|
6
|
+
export const isDev = (() => {
|
|
7
|
+
try {
|
|
8
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
|
|
9
|
+
if (typeof import.meta !== 'undefined' && import.meta.env?.PROD === true) return false;
|
|
10
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
|
|
11
|
+
if (
|
|
12
|
+
typeof import.meta !== 'undefined' &&
|
|
13
|
+
(import.meta.env?.DEV === true || import.meta.env?.NODE_ENV === 'development')
|
|
14
|
+
)
|
|
15
|
+
return true;
|
|
16
|
+
// Bare browser import with no bundler — treat localhost as dev so warnings surface
|
|
17
|
+
if (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if the current device is a mac
|
|
27
|
+
* @returns {boolean} isMac
|
|
28
|
+
*/
|
|
29
|
+
export const isMac = () =>
|
|
30
|
+
// eslint-disable-next-line compat/compat
|
|
31
|
+
(window.navigator?.userAgentData?.platform || window.navigator.platform).toLowerCase().startsWith('mac');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copy a piece of text to the clipboard
|
|
35
|
+
* @param {string} text - The text to copy to the clipboard
|
|
36
|
+
* @returns {boolean} A boolean indicating the success of the action
|
|
37
|
+
*/
|
|
38
|
+
export const copyToClipboard = text => {
|
|
39
|
+
if (window.isSecureContext && navigator.clipboard) {
|
|
40
|
+
navigator.clipboard.writeText(text);
|
|
41
|
+
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const textarea = document.createElement('textarea');
|
|
46
|
+
|
|
47
|
+
textarea.value = text ?? '';
|
|
48
|
+
textarea.style.position = 'fixed';
|
|
49
|
+
textarea.style.opacity = '0';
|
|
50
|
+
document.body.appendChild(textarea);
|
|
51
|
+
textarea.select();
|
|
52
|
+
|
|
53
|
+
let ok = false;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
ok = document.execCommand('copy');
|
|
57
|
+
} catch {
|
|
58
|
+
// execCommand not supported
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
textarea.remove();
|
|
62
|
+
|
|
63
|
+
return ok;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the current text from the clipboard
|
|
68
|
+
* @returns {string} The current clipboard content
|
|
69
|
+
*/
|
|
70
|
+
export const readClipboard = async () => {
|
|
71
|
+
if (!window.isSecureContext || !navigator.clipboard) return false;
|
|
72
|
+
|
|
73
|
+
return await window.navigator.clipboard.readText();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Vibrate the device (if supported)
|
|
78
|
+
* @param {number|[...number]} durationPattern - The duration(s) to vibrate (for an array: odd indexes are vibrate and even are pause)
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
export const vibrate = (durationPattern = 30) => {
|
|
82
|
+
if (!navigator.vibrate) return;
|
|
83
|
+
|
|
84
|
+
// eslint-disable-next-line compat/compat
|
|
85
|
+
navigator.vibrate(durationPattern);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Vibrate the device for a short burst to serve as a tactile response (if supported)
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
export const tactileResponse = () => vibrate(30);
|
package/utils/class.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate secure unique ID that is safe to use as a class name or id
|
|
5
|
+
* @param {number} size - The size of the ID. The default size is 10
|
|
6
|
+
* @returns {string} Random id string
|
|
7
|
+
*/
|
|
8
|
+
export const classSafeNanoid = (size = 10) =>
|
|
9
|
+
// eslint-disable-next-line spellcheck/spell-checker
|
|
10
|
+
customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-', size)();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert any number of strings or arrays into a single array with no duplicates
|
|
14
|
+
* @param {...any} classNames - strings or arrays of strings
|
|
15
|
+
* @returns {Array} classList
|
|
16
|
+
*/
|
|
17
|
+
export const buildClassList = (...classNames) => [
|
|
18
|
+
...new Set(
|
|
19
|
+
classNames
|
|
20
|
+
.flat(Number.POSITIVE_INFINITY)
|
|
21
|
+
.filter(className => typeof className === 'string' && className.length > 0 && !/^\s+$/.test(className)),
|
|
22
|
+
),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert any number of strings or arrays into a single space delineated string with no duplicates
|
|
27
|
+
* @param {...any} classNames - strings or arrays of strings
|
|
28
|
+
* @returns {string} className
|
|
29
|
+
*/
|
|
30
|
+
export const buildClassName = (...classNames) => buildClassList(...classNames).join(' ');
|
package/utils/color.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { random as randomColor } from '@ctrl/tinycolor';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a hsl color string using a string as the seed
|
|
5
|
+
* @param {string} string - The string to use as the color seed
|
|
6
|
+
* @param {object} config - Min/Max constraints for the color
|
|
7
|
+
* @param {[number, number]} config.h - The hue constraints [min, max]
|
|
8
|
+
* @param {[number, number]} config.s - The saturation constraints [min, max]
|
|
9
|
+
* @param {[number, number]} config.l - The lightness constraints [min, max]
|
|
10
|
+
* @returns {string} hsl color string
|
|
11
|
+
*/
|
|
12
|
+
export const stringToColor = (string, config = {}) => {
|
|
13
|
+
const { h, s, l } = { h: [0, 360], s: [75, 100], l: [40, 60], ...config };
|
|
14
|
+
|
|
15
|
+
if (!string) return randomColor().toRgbString();
|
|
16
|
+
|
|
17
|
+
const range = (hash, min, max) => {
|
|
18
|
+
const diff = max - min;
|
|
19
|
+
const x = ((hash % diff) + diff) % diff;
|
|
20
|
+
|
|
21
|
+
return x + min;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let hash = 0;
|
|
25
|
+
|
|
26
|
+
for (let x = 0; x < string.length; ++x) {
|
|
27
|
+
hash = string.codePointAt(x) + ((hash << 5) - hash);
|
|
28
|
+
hash &= hash;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `hsl(${range(hash, h[0], h[1])}, ${range(hash, s[0], s[1])}%, ${range(hash, l[0], l[1])}%)`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const rgb2lab = rgb => {
|
|
35
|
+
let r = rgb[0] / 255,
|
|
36
|
+
g = rgb[1] / 255,
|
|
37
|
+
b = rgb[2] / 255,
|
|
38
|
+
x,
|
|
39
|
+
y,
|
|
40
|
+
z;
|
|
41
|
+
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
|
|
42
|
+
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
|
|
43
|
+
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
|
|
44
|
+
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
|
45
|
+
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1;
|
|
46
|
+
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
|
47
|
+
x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
|
|
48
|
+
y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
|
|
49
|
+
z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;
|
|
50
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the delta between 2 rgb colors
|
|
55
|
+
* @param {[number, number, number]} rgbA - The rgb values (0-255) in an array [r, g, b]
|
|
56
|
+
* @param {[number, number, number]} rgbB - The rgb values (0-255) in an array [r, g, b]
|
|
57
|
+
* @returns {number} The color delta
|
|
58
|
+
*/
|
|
59
|
+
export const rgbDelta = (rgbA, rgbB) => {
|
|
60
|
+
const labA = rgb2lab(rgbA);
|
|
61
|
+
const labB = rgb2lab(rgbB);
|
|
62
|
+
const deltaL = labA[0] - labB[0];
|
|
63
|
+
const deltaA = labA[1] - labB[1];
|
|
64
|
+
const deltaB = labA[2] - labB[2];
|
|
65
|
+
const c1 = Math.hypot(labA[1], labA[2]);
|
|
66
|
+
const c2 = Math.hypot(labB[1], labB[2]);
|
|
67
|
+
const deltaC = c1 - c2;
|
|
68
|
+
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
|
|
69
|
+
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
|
|
70
|
+
const sh = 1 + 0.015 * c1;
|
|
71
|
+
|
|
72
|
+
/* eslint-disable spellcheck/spell-checker */
|
|
73
|
+
const sc = 1 + 0.045 * c1;
|
|
74
|
+
const deltaLKlsl = deltaL / 1;
|
|
75
|
+
const deltaCkcsc = deltaC / sc;
|
|
76
|
+
const deltaHkhsh = deltaH / sh;
|
|
77
|
+
const index = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
|
|
78
|
+
/* eslint-enable spellcheck/spell-checker */
|
|
79
|
+
|
|
80
|
+
return index < 0 ? 0 : Math.sqrt(index);
|
|
81
|
+
};
|
package/utils/data.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { isDev } from './browser';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates debounced version of function that delays execution until after delay period of inactivity.
|
|
5
|
+
* @param {Function} callback - Function to debounce
|
|
6
|
+
* @param {number} [delay] - Milliseconds to delay after last invocation
|
|
7
|
+
* @returns {Function} Debounced function that cancels previous invocations
|
|
8
|
+
*/
|
|
9
|
+
export const debounce = (callback, delay = 400) => {
|
|
10
|
+
let timerId;
|
|
11
|
+
|
|
12
|
+
return (...args) => {
|
|
13
|
+
clearTimeout(timerId);
|
|
14
|
+
|
|
15
|
+
timerId = setTimeout(() => callback(...args), delay);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates throttled version of function that limits execution to once per delay period.
|
|
21
|
+
* @param {Function} callback - Function to throttle
|
|
22
|
+
* @param {number} [delay] - Minimum milliseconds between function invocations
|
|
23
|
+
* @returns {Function} Throttled function that enforces rate limiting
|
|
24
|
+
*/
|
|
25
|
+
export const throttle = (callback, delay = 400) => {
|
|
26
|
+
let previousCall = 0;
|
|
27
|
+
|
|
28
|
+
return function () {
|
|
29
|
+
const time = Date.now();
|
|
30
|
+
|
|
31
|
+
if (time - previousCall >= delay) {
|
|
32
|
+
previousCall = time;
|
|
33
|
+
Reflect.apply(callback, null, arguments);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates promise that resolves after specified delay.
|
|
40
|
+
* @param {number} ms - Milliseconds to delay before promise resolution
|
|
41
|
+
* @returns {Promise<void>} Promise that resolves after delay period
|
|
42
|
+
*/
|
|
43
|
+
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes function with automatic retry on failure, with configurable delay and attempt limits.
|
|
47
|
+
* @param {Function} callback - Function to execute and retry on failure
|
|
48
|
+
* @param {object} [options] - Retry configuration
|
|
49
|
+
* @param {number|Function} [options.delay] - Delay in milliseconds between retries, or function receiving attempt index
|
|
50
|
+
* @param {number} [options.max] - Maximum number of retry attempts
|
|
51
|
+
* @param {number} [options.index] - Internal retry counter, do not provide
|
|
52
|
+
* @returns {Promise<*>} Promise resolving to callback return value
|
|
53
|
+
* @throws {Error} Final error if all retry attempts fail
|
|
54
|
+
*/
|
|
55
|
+
export const retry = async (callback, options = {}) => {
|
|
56
|
+
const { index = 0, max = 3 } = options;
|
|
57
|
+
const delayMs = (typeof options.delay === 'function' ? options.delay(index) : options.delay) ?? 500;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return await callback();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
if (isDev) console.warn('[DEV] retry error', error);
|
|
64
|
+
|
|
65
|
+
if (index >= max) throw error;
|
|
66
|
+
|
|
67
|
+
await delay(delayMs);
|
|
68
|
+
|
|
69
|
+
return retry(callback, { ...options, index: index + 1 });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts number from source range to equivalent value in target range using linear interpolation.
|
|
75
|
+
* @param {number} value - Number to convert between ranges
|
|
76
|
+
* @param {[number, number]} sourceRange - Source range as [min, max] array
|
|
77
|
+
* @param {[number, number]} targetRange - Target range as [min, max] array
|
|
78
|
+
* @returns {number} Value converted to target range maintaining proportional position
|
|
79
|
+
*/
|
|
80
|
+
export const convertRange = (value, sourceRange, targetRange) =>
|
|
81
|
+
((value - sourceRange[0]) * (targetRange[1] - targetRange[0])) / (sourceRange[1] - sourceRange[0]) + targetRange[0];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extracts non-native property names from object and its prototype chain.
|
|
85
|
+
*
|
|
86
|
+
* Filters out built-in JavaScript object methods and properties like constructor,
|
|
87
|
+
* toString, hasOwnProperty, etc.
|
|
88
|
+
* @param {object} object - Object to extract custom properties from
|
|
89
|
+
* @returns {string[]} Array of custom property names excluding native methods
|
|
90
|
+
*/
|
|
91
|
+
export const getCustomProperties = object =>
|
|
92
|
+
[
|
|
93
|
+
...new Set(object ? [...Reflect.ownKeys(object), ...getCustomProperties(Object.getPrototypeOf(object))] : []),
|
|
94
|
+
].filter(
|
|
95
|
+
key =>
|
|
96
|
+
typeof key !== 'string' ||
|
|
97
|
+
!/^(?:constructor|prototype|arguments|caller|name|length|toString|toLocaleString|valueOf|apply|bind|call|__proto__|__defineGetter__|__defineSetter__|hasOwnProperty|__lookupGetter__|__lookupSetter__|isPrototypeOf|propertyIsEnumerable)$/.test(
|
|
98
|
+
key,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds array from conditional item descriptors, supporting conditional inclusion and arrays.
|
|
104
|
+
* @param {object[]} conditionalItems - Array of conditional item descriptors
|
|
105
|
+
* @param {boolean} conditionalItems[].if - Condition determining item inclusion
|
|
106
|
+
* @param {*} [conditionalItems[].thenItem] - Single item added when condition is true
|
|
107
|
+
* @param {*[]} [conditionalItems[].thenItems] - Array of items spread when condition is true
|
|
108
|
+
* @param {*} [conditionalItems[].elseItem] - Single item added when condition is false
|
|
109
|
+
* @param {*[]} [conditionalItems[].elseItems] - Array of items spread when condition is false
|
|
110
|
+
* @param {*} [conditionalItems[].alwaysItem] - Single item always added regardless of condition
|
|
111
|
+
* @param {*[]} [conditionalItems[].alwaysItems] - Array of items always spread regardless of condition
|
|
112
|
+
* @returns {*[]} Flattened array of items matching their conditions
|
|
113
|
+
*/
|
|
114
|
+
export const conditionalList = conditionalItems =>
|
|
115
|
+
conditionalItems
|
|
116
|
+
.filter(
|
|
117
|
+
item =>
|
|
118
|
+
item.if ||
|
|
119
|
+
item.elseItem ||
|
|
120
|
+
Array.isArray(item.elseItems) ||
|
|
121
|
+
Array.isArray(item.alwaysItems) ||
|
|
122
|
+
item.alwaysItem !== undefined,
|
|
123
|
+
)
|
|
124
|
+
.flatMap(item => {
|
|
125
|
+
if (item.alwaysItem !== undefined) return [item.alwaysItem];
|
|
126
|
+
if (item.if && item.thenItem !== undefined) return [item.thenItem];
|
|
127
|
+
if (!item.if && item.elseItem !== undefined) return [item.elseItem];
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(item.alwaysItems)) return item.alwaysItems;
|
|
130
|
+
if (item.if && Array.isArray(item.thenItems)) return item.thenItems;
|
|
131
|
+
if (!item.if && Array.isArray(item.elseItems)) return item.elseItems;
|
|
132
|
+
|
|
133
|
+
return [];
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates multi-level sorting function for Array.sort() with locale-aware comparison.
|
|
138
|
+
* @param {object|object[]} orders - Sort configuration, single object or array for multi-level sorting
|
|
139
|
+
* @param {string} [orders.property] - Object property to sort by, undefined sorts primitive values directly
|
|
140
|
+
* @param {'asc'|'desc'} [orders.direction] - Sort direction: ascending or descending
|
|
141
|
+
* @returns {Function} Comparator function for Array.sort() with multi-level sorting support
|
|
142
|
+
*/
|
|
143
|
+
export const orderBy = orders => (a, b) => {
|
|
144
|
+
const sortDirection = { asc: -1, desc: 1 };
|
|
145
|
+
const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
146
|
+
|
|
147
|
+
if (!Array.isArray(orders)) orders = [orders];
|
|
148
|
+
|
|
149
|
+
const totalOrders = orders.length;
|
|
150
|
+
|
|
151
|
+
for (let index = 0; index < totalOrders; index++) {
|
|
152
|
+
const { property, direction = 'asc' } = orders[index];
|
|
153
|
+
const directionInt = sortDirection[direction];
|
|
154
|
+
const compare = sortCollator.compare(
|
|
155
|
+
property === undefined ? a : a[property],
|
|
156
|
+
property === undefined ? b : b[property],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (compare < 0) return directionInt;
|
|
160
|
+
if (compare > 0) return -directionInt;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return 0;
|
|
164
|
+
};
|
package/utils/element.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates zero-based index position of element within parent's children.
|
|
3
|
+
* @param {HTMLElement} element - Element to find index of
|
|
4
|
+
* @param {number} [index] - Internal recursion accumulator, do not provide
|
|
5
|
+
* @returns {number} Zero-based index position within parent element
|
|
6
|
+
*/
|
|
7
|
+
export const getElementIndex = (element, index = 0) => {
|
|
8
|
+
if (element.previousElementSibling) return getElementIndex(element.previousElementSibling, ++index);
|
|
9
|
+
|
|
10
|
+
return index;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Determines if element is a descendant of specified parent element.
|
|
15
|
+
* @param {HTMLElement} element - Element to test descendant relationship
|
|
16
|
+
* @param {HTMLElement} parentElement - Potential ancestor element
|
|
17
|
+
* @returns {boolean} True if element is descendant of parentElement, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
export const isDescendantOf = (element, parentElement) => {
|
|
20
|
+
const isTheParent = element.parentElement === parentElement;
|
|
21
|
+
|
|
22
|
+
return !isTheParent && element.parentElement?.parentElement
|
|
23
|
+
? isDescendantOf(element.parentElement, parentElement)
|
|
24
|
+
: isTheParent;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds elements containing specified text using XPath evaluation.
|
|
29
|
+
*
|
|
30
|
+
* Uses XPath contains() function with optional case-insensitive matching.
|
|
31
|
+
* Returns deepest matching elements to avoid duplicate parent/child results.
|
|
32
|
+
* @param {string} text - Text content to search for within elements
|
|
33
|
+
* @param {object} [options] - Search configuration options
|
|
34
|
+
* @param {string} [options.xPathElement] - Element selector for XPath evaluation
|
|
35
|
+
* @param {Node} [options.scope] - Context node to scope the search query
|
|
36
|
+
* @param {boolean} [options.caseSensitive] - Whether to perform case-sensitive text matching
|
|
37
|
+
* @returns {Node[]} Array of elements containing the text, with duplicate parent/child pairs resolved to deepest match
|
|
38
|
+
*/
|
|
39
|
+
export const getElementsContainingText = (text, options = {}) => {
|
|
40
|
+
const { xPathElement = '*', scope = document.body, caseSensitive = false } = options;
|
|
41
|
+
|
|
42
|
+
const esc = str => (!str.includes("'") ? `'${str}'` : `concat('${str.replace(/'/g, "',\"'\",'")}')`);
|
|
43
|
+
|
|
44
|
+
const xPath = `.//${xPathElement}[contains(${caseSensitive ? `text(),${esc(text)}` : `translate(.,${esc(text.toUpperCase())},${esc(text.toLowerCase())}),${esc(text.toLowerCase())}`})]`;
|
|
45
|
+
const result = document.evaluate(xPath, scope, null, XPathResult.ANY_TYPE, null);
|
|
46
|
+
|
|
47
|
+
let node = null;
|
|
48
|
+
const nodes = [];
|
|
49
|
+
while ((node = result.iterateNext())) {
|
|
50
|
+
if (nodes.length > 0 && nodes.at(-1).contains(node)) nodes[nodes.length - 1] = node;
|
|
51
|
+
else nodes.push(node);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return nodes;
|
|
55
|
+
};
|
package/utils/index.js
ADDED
package/utils/rand.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @type {(min: number, max: number) => number} Generate a random number from a range (min inclusive, max exclusive) */
|
|
2
|
+
export const rand = (min = -999, max = 999) => Math.random() * (max - min) + min;
|
|
3
|
+
|
|
4
|
+
/** @type {(min: number, max: number) => number} Generate a random integer from a range (min & max inclusive) */
|
|
5
|
+
export const randInt = (min = -999, max = 999) => {
|
|
6
|
+
min = Math.ceil(min);
|
|
7
|
+
|
|
8
|
+
return Math.floor(Math.random() * (Math.floor(max) - min + 1) + min);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** @type {(array: Array) => number} Choose a random item from an array */
|
|
12
|
+
export const randFromArray = array => array[randInt(0, array.length - 1)];
|
package/utils/string.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capitalize the string
|
|
3
|
+
* @param {string} string - The source string
|
|
4
|
+
* @param {boolean} recursive - Weather or not to recurse over every word in the string
|
|
5
|
+
* @param {string} split - The string used to split apart the source string (if using recursive)
|
|
6
|
+
* @returns {string} The capitalized string
|
|
7
|
+
*/
|
|
8
|
+
export const capitalize = (string, recursive, split = ' ') => {
|
|
9
|
+
const words = string.split(split);
|
|
10
|
+
const wordCount = words.length;
|
|
11
|
+
|
|
12
|
+
for (let x = 0, word; x < (recursive ? wordCount : 1); ++x) {
|
|
13
|
+
word = words[x];
|
|
14
|
+
|
|
15
|
+
words[x] = word.charAt(0).toUpperCase() + word.slice(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return words.join(split);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reformat a camelCase or PascalCase string with a custom joiner
|
|
23
|
+
* @param {string} string - The source string
|
|
24
|
+
* @param {string} joiner - The string used to re-join the words
|
|
25
|
+
* @returns {string} The re-formatted string
|
|
26
|
+
*/
|
|
27
|
+
export const fromCamelCase = (string, joiner = ' ') => {
|
|
28
|
+
return string.split(/(?=[A-Z][a-z])/).join(joiner);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reformat string to a camelCase
|
|
33
|
+
* @param {string} string - The source string
|
|
34
|
+
* @param {string} splitter - The string used to split the source into words
|
|
35
|
+
* @returns {string} The re-formatted string
|
|
36
|
+
*/
|
|
37
|
+
export const toCamelCase = (string, splitter = ' ') => {
|
|
38
|
+
return (string.charAt(0).toLowerCase() + string.slice(1))
|
|
39
|
+
.split(splitter)
|
|
40
|
+
.map((item, index) => (index > 0 ? capitalize(item) : item))
|
|
41
|
+
.join('');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reformat string to a PascalCase
|
|
46
|
+
* @param {string} string - The source string
|
|
47
|
+
* @returns {string} The re-formatted string
|
|
48
|
+
*/
|
|
49
|
+
export const toPascalCase = (string = '') => capitalize(toCamelCase(string));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strip all excess newline and indentation whitespace from the source string
|
|
53
|
+
* @param {string} string - The source string
|
|
54
|
+
* @returns {string} The re-formatted string
|
|
55
|
+
*/
|
|
56
|
+
export const removeExcessIndentation = string => {
|
|
57
|
+
if (!string.includes('\t')) return string;
|
|
58
|
+
if (!string.includes('\n')) return string.replace(/^\t+/, '');
|
|
59
|
+
|
|
60
|
+
const lines = string.split('\n');
|
|
61
|
+
|
|
62
|
+
const minIndentation = lines
|
|
63
|
+
.map(line => line.match(/^\t+/) || [])
|
|
64
|
+
.reduce((a, b) => {
|
|
65
|
+
if (!a?.length) return b;
|
|
66
|
+
if (!b?.length) return a;
|
|
67
|
+
|
|
68
|
+
return a[0].length <= b[0].length ? a : b;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return lines.join('\n').replaceAll(new RegExp(`^${minIndentation[0]}`, 'gm'), '');
|
|
72
|
+
};
|