@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.
Files changed (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. package/utils/string.js +72 -0
@@ -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.
@@ -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
+ };
@@ -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
@@ -0,0 +1,7 @@
1
+ export * from './browser';
2
+ export * from './class';
3
+ export * from './color';
4
+ export * from './data';
5
+ export * from './element';
6
+ export * from './rand';
7
+ export * from './string';
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)];
@@ -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
+ };