@splendidlabz/utils 1.5.0-alpha.1 → 1.5.0-alpha.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @splendidlabz/utils
2
2
 
3
+ ## 1.5.0-alpha.3
4
+
5
+ ### Minor Changes
6
+
7
+ - 5fe7c51: - Just bumping
8
+ - Fix bug with `mix` erroring when a `null` or `undefined` source is passed to it. `mix` now ignores `null` and `undefined`.
9
+ - Adds `toDegrees`
10
+ - Adds `toRadians`
11
+ - Adds `getCSSValue` and `setCSSValue`. They're aliases for `getCSSVar` and `setCSSVar` but they're more descriptive for standard properties.
12
+ - Renamed `hasLabel` to `checkForAccessibleName`
13
+ - Add `getParentElement` to get the parent element of a DOM node. If it encounters an `ASTRO-ISLAND` and `ASTRO-SLOT`, it goes one level up.
14
+
15
+ ## 1.5.0-alpha.2
16
+
17
+ ### Minor Changes
18
+
19
+ - acf9faf: Improve sort function
20
+
3
21
  ## 1.5.0-alpha.1
4
22
 
5
23
  ### Patch Changes
@@ -1,6 +1,6 @@
1
- import { getChildrenElements, mediaLoaded } from '../dom'
1
+ import { getChildrenElements, mediaLoaded } from '../dom/index.js'
2
2
 
3
- import { resizeObserver } from './resize-observer'
3
+ import { resizeObserver } from './resize-observer.js'
4
4
 
5
5
  export async function masonry(node) {
6
6
  if (nativeMasonrySupport(node)) return
@@ -37,39 +37,6 @@ export async function masonry(node) {
37
37
  }
38
38
  }
39
39
 
40
- // export async function masonry(node) {
41
- // if (typeof window === 'undefined') return
42
- // if (nativeMasonrySupport(node)) return
43
-
44
- // let items
45
- // let gap
46
-
47
- // node.style.setProperty('row-gap', '1px', 'important')
48
- // node.style.gridAutoRows = '0px'
49
- // // await mediaLoaded(node)
50
-
51
- // const obs = resizeObserver(node, {
52
- // async callback(opts) {
53
- // items = getChildrenElements(node)
54
- // gap = parseFloat(getComputedStyle(node).columnGap)
55
- // layout(node)
56
- // },
57
- // })
58
-
59
- // async function layout() {
60
- // items.forEach(item => {
61
- // const itemBox = item.getBoundingClientRect()
62
- // // item.style.gridRowEnd = `span ${Math.round(itemBox.height + gap)}`
63
- // })
64
- // }
65
-
66
- // return {
67
- // destroy() {
68
- // obs.disconnect()
69
- // },
70
- // }
71
- // }
72
-
73
40
  function nativeMasonrySupport(node) {
74
41
  return getComputedStyle(node).gridTemplateRows === 'masonry'
75
42
  }
@@ -1,21 +1,32 @@
1
- export function stopBodyScroll() {
2
- const top = document.documentElement.scrollTop
1
+ // export function stopBodyScroll(element) {
2
+ // if (!element) element = document.body
3
+ // const top = document.documentElement.scrollTop
4
+ // element.style.top = -1 * top + 'px'
5
+ // element.style.overflow = 'hidden'
6
+ // }
3
7
 
4
- element.style.top = -1 * top + 'px'
5
- element.style.overflow = 'hidden'
6
- }
8
+ // export function allowBodyScroll() {
9
+ // const top = parseFloat(document.body.style.top) * -1
10
+ // document.body.style.top = null
11
+ // document.body.style.overflow = null
12
+ // document.documentElement.scrollTop = top
13
+ // }
7
14
 
8
- export function allowBodyScroll() {
9
- const top = parseFloat(document.body.style.top) * -1
10
- document.body.style.top = null
11
- document.body.style.overflow = null
12
- document.documentElement.scrollTop = top
15
+ export function hasAccessibleName(label, labelledBy) {
16
+ if (label || labelledBy) return true
13
17
  }
14
18
 
15
- export function inertOthers() {}
19
+ export function checkForAccessibleName({
20
+ name,
21
+ ariaLabel,
22
+ ariaLabelledBy,
23
+ severity = 'warn',
24
+ } = {}) {
25
+ if (ariaLabel || ariaLabelledBy) return true
16
26
 
17
- export function hasLabel(label, labelledBy) {
18
- if (label || labelledBy) return true
27
+ const message = `${name} is missing an accessible name. Please provide an accessible name with 'aria-label' or 'aria-labelledby'.`
28
+ if (severity === 'warn') console.warn(message)
29
+ if (severity === 'error') console.error(message)
19
30
  }
20
31
 
21
32
  export function isInvalidAriaPopup(value) {
package/dom/css-vars.js CHANGED
@@ -1,5 +1,13 @@
1
1
  import { getElement } from './get-element.js'
2
2
 
3
+ export function getCSSValue(element, prop) {
4
+ return getCSSVar(element, prop)
5
+ }
6
+
7
+ export function setCSSValue(element, prop, value) {
8
+ setCSSVar(element, prop, value)
9
+ }
10
+
3
11
  export function getCSSVar(element, prop) {
4
12
  element = getElement(element)
5
13
  return getComputedStyle(element).getPropertyValue(prop)
package/dom/font-size.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { splitUnit } from '../lib/index.js'
2
2
 
3
- /* globals getComputedStyle */
3
+ export function getUnit(value) {
4
+ const [, unit] = splitUnit(value)
5
+ return unit
6
+ }
7
+
4
8
  export function em(element = document.body, multiple = 1) {
5
9
  const unit = parseFloat(getComputedStyle(element)['font-size'])
6
10
  return Math.round(unit * multiple)
@@ -12,13 +16,13 @@ export function rem(multiple = 1) {
12
16
  }
13
17
 
14
18
  export function toPx(value, element = null) {
15
- const [amt, unit] = splitUnit(value)
19
+ const [numeric, unit] = splitUnit(value)
16
20
  let finalValue = 0
17
21
 
18
- if (unit === 'rem') finalValue = rem(amt)
19
- if (unit === 'em') finalValue = em(element, amt)
20
- if (unit === 'px') finalValue = amt
21
- if (!unit) finalValue = amt // Default to px offsets
22
+ if (unit === 'rem') finalValue = rem(numeric)
23
+ if (unit === 'em') finalValue = em(element, numeric)
24
+ if (unit === 'px') finalValue = numeric
25
+ if (!unit) finalValue = numeric // Default to px offsets
22
26
 
23
27
  return finalValue
24
28
  }
@@ -2,6 +2,8 @@
2
2
  // If the selector is already an HTMLElement, it will return the selector itself.
3
3
  // Otherwise, it will return the first element that matches the selector.
4
4
 
5
+ const astroNodes = ['ASTRO-SLOT', 'ASTRO-ISLAND']
6
+
5
7
  export function getElement(selector) {
6
8
  if (!selector) return
7
9
  if (selector instanceof HTMLElement) return selector
@@ -16,6 +18,12 @@ export function getChildrenElements(node) {
16
18
  return Array.from(children)
17
19
  }
18
20
 
21
+ export function getParentElement(element) {
22
+ const parent = element.parentElement
23
+ if (astroNodes.includes(parent.nodeName)) return getParentElement(parent)
24
+ return parent
25
+ }
26
+
19
27
  export function getSiblingElements(element) {
20
28
  return Array.from(element.parentElement.children).filter(
21
29
  child => child !== element,
@@ -0,0 +1,2 @@
1
+ import config from '@splendidlabz/eslint-config'
2
+ export default config
@@ -1,3 +1,5 @@
1
+ import { getNestedValue } from '../objects/nested-property.js'
2
+
1
3
  export function last(array, index) {
2
4
  return index === array.length - 1
3
5
  }
@@ -25,38 +27,59 @@ export function shuffle(array) {
25
27
  }
26
28
 
27
29
  /**
28
- * Sorts an array of objects or strings without mutating the array
30
+ * Sorts an array of objects, numbers, or strings without mutating the original array
31
+ * @param {Array} array - Array to sort
32
+ * @param {Object} options - Sort options
33
+ * @param {string|string[]|null} options.props - Property or array of properties to sort by. If null, sorts simple arrays
34
+ * @param {boolean} options.reverse - Whether to sort in reverse order
29
35
  */
30
- export function sort(
31
- array,
32
- {
33
- property, // Property to sort by. Used when sorting array of objects.
34
- order = 'asc', // 'asc' or 'desc'
35
- },
36
- ) {
36
+ export function sort(array, { props = null, reverse = false } = {}) {
37
37
  const clone = array.slice()
38
- const sorted = clone.sort((a, b) => {
39
- let one = a
40
- let two = b
41
-
42
- // Use the `sortBy` property if sorting by objects
43
- if (property) {
44
- one = a[property]
45
- two = b[property]
46
- }
47
38
 
48
- if (order === 'asc') {
49
- if (one < two) return -1
50
- if (one > two) return 1
51
- }
39
+ // Handle simple arrays (numbers or strings)
40
+ if (props === null) {
41
+ return clone.sort((a, b) => {
42
+ const comparison = compareValues(a, b)
43
+ return reverse ? -comparison : comparison
44
+ })
45
+ }
52
46
 
53
- if (order === 'desc') {
54
- if (one < two) return 1
55
- if (one > two) return -1
56
- }
47
+ // Handle objects in arrays
48
+ const properties = Array.isArray(props) ? props : [props]
49
+
50
+ return clone.sort((a, b) => {
51
+ for (const property of properties) {
52
+ const aValue = getNestedValue(a, property)
53
+ const bValue = getNestedValue(b, property)
54
+
55
+ // Skip if values are equal
56
+ if (aValue === bValue) continue
57
57
 
58
+ // Push null/undefined values to the end
59
+ if (aValue == null) return 1
60
+ if (bValue == null) return -1
61
+
62
+ // Compare values and handle reverse sort
63
+ const comparison = compareValues(aValue, bValue)
64
+ return reverse ? -comparison : comparison
65
+ }
58
66
  return 0
59
67
  })
68
+ }
69
+
70
+ // Compare values for sorting
71
+ function compareValues(a, b) {
72
+ // Compare numbers
73
+ if (typeof a === 'number' && typeof b === 'number') return a - b
74
+
75
+ // Compare strings
76
+ if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b)
77
+
78
+ // Compare dates
79
+ const aDate = new Date(a)
80
+ const bDate = new Date(b)
81
+ if (!isNaN(aDate) && !isNaN(bDate)) return aDate.getTime() - bDate.getTime()
60
82
 
61
- return sorted
83
+ // Convert into strings and compare
84
+ return String(a).localeCompare(String(b))
62
85
  }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { sort } from './index.js'
4
+
5
+ describe('Sort by', _ => {
6
+ it('Standard array - numbers', async () => {
7
+ const array = [3, 1, 2]
8
+ const sorted = sort(array)
9
+ const reversed = sort(array, { reverse: true })
10
+ expect(sorted).toEqual([1, 2, 3])
11
+ expect(reversed).toEqual([3, 2, 1])
12
+ })
13
+
14
+ it('Standard array - strings', async () => {
15
+ const array = ['c', 'a', 'b']
16
+ const sorted = sort(array)
17
+ const reversed = sort(array, { reverse: true })
18
+ expect(sorted).toEqual(['a', 'b', 'c'])
19
+ expect(reversed).toEqual(['c', 'b', 'a'])
20
+ })
21
+
22
+ it('Standard array - dates', async () => {
23
+ const array = [
24
+ new Date('2021-05-18'),
25
+ new Date('2022-01-01'),
26
+ new Date('2025-03-09'),
27
+ ]
28
+ const sorted = sort(array)
29
+ const reversed = sort(array, { reverse: true })
30
+ expect(sorted).toEqual([
31
+ new Date('2021-05-18'),
32
+ new Date('2022-01-01'),
33
+ new Date('2025-03-09'),
34
+ ])
35
+ expect(reversed).toEqual([
36
+ new Date('2025-03-09'),
37
+ new Date('2022-01-01'),
38
+ new Date('2021-05-18'),
39
+ ])
40
+ })
41
+ it('Array of objects — simple integer', async () => {
42
+ const array = [
43
+ { name: 'Ace', age: 30 },
44
+ { name: 'Betty', age: 25 },
45
+ { name: 'Candy', age: 35 },
46
+ ]
47
+ const sorted = sort(array, { props: 'age' })
48
+ const reversed = sort(array, { props: 'age', reverse: true })
49
+ expect(sorted).toEqual([
50
+ { name: 'Betty', age: 25 },
51
+ { name: 'Ace', age: 30 },
52
+ { name: 'Candy', age: 35 },
53
+ ])
54
+ expect(reversed).toEqual([
55
+ { name: 'Candy', age: 35 },
56
+ { name: 'Ace', age: 30 },
57
+ { name: 'Betty', age: 25 },
58
+ ])
59
+ })
60
+
61
+ it('Array of objects — simple string', async () => {
62
+ const array = [
63
+ { name: 'Ace', age: 30 },
64
+ { name: 'Betty', age: 25 },
65
+ { name: 'Candy', age: 35 },
66
+ ]
67
+ const sorted = sort(array, { props: 'name' })
68
+ const reversed = sort(array, { props: 'name', reverse: true })
69
+ expect(sorted).toEqual([
70
+ { name: 'Ace', age: 30 },
71
+ { name: 'Betty', age: 25 },
72
+ { name: 'Candy', age: 35 },
73
+ ])
74
+ expect(reversed).toEqual([
75
+ { name: 'Candy', age: 35 },
76
+ { name: 'Betty', age: 25 },
77
+ { name: 'Ace', age: 30 },
78
+ ])
79
+ })
80
+
81
+ it('Double property', async () => {
82
+ const array = [
83
+ { name: 'Ace', age: 30 },
84
+ { name: 'Betty', age: 25 },
85
+ { name: 'Candy', age: 35 },
86
+ { name: 'Danny', age: 30 },
87
+ ]
88
+ const sorted = sort(array, { props: ['age', 'name'] })
89
+ const reversed = sort(array, { props: ['age', 'name'], reverse: true })
90
+ expect(sorted).toEqual([
91
+ { name: 'Betty', age: 25 },
92
+ { name: 'Ace', age: 30 },
93
+ { name: 'Danny', age: 30 },
94
+ { name: 'Candy', age: 35 },
95
+ ])
96
+ expect(reversed).toEqual([
97
+ { name: 'Candy', age: 35 },
98
+ { name: 'Danny', age: 30 },
99
+ { name: 'Ace', age: 30 },
100
+ { name: 'Betty', age: 25 },
101
+ ])
102
+ })
103
+
104
+ it('Nested value', async () => {
105
+ const array = [
106
+ { data: { value: 1 } },
107
+ { data: { value: 2 } },
108
+ { data: { value: 3 } },
109
+ ]
110
+ const sorted = sort(array, { props: 'data.value' })
111
+ const reversed = sort(array, { props: 'data.value', reverse: true })
112
+ expect(sorted).toEqual([
113
+ { data: { value: 1 } },
114
+ { data: { value: 2 } },
115
+ { data: { value: 3 } },
116
+ ])
117
+ expect(reversed).toEqual([
118
+ { data: { value: 3 } },
119
+ { data: { value: 2 } },
120
+ { data: { value: 1 } },
121
+ ])
122
+ })
123
+ })
@@ -1,3 +1,5 @@
1
+ export * from './math.js'
2
+
1
3
  export function splitUnit(string) {
2
4
  // If there are no units, return the number straight.
3
5
  if (typeof string === 'number') return [string, null]
@@ -0,0 +1,9 @@
1
+ // Convert radians to degrees
2
+ export function toDegrees(angle) {
3
+ return angle * (180 / Math.PI)
4
+ }
5
+
6
+ // Convert degrees to radians
7
+ export function toRadians(angle) {
8
+ return angle * (Math.PI / 180)
9
+ }
@@ -1,6 +1,8 @@
1
1
  export function mix(...sources) {
2
2
  const result = {}
3
3
  for (const source of sources) {
4
+ // Skip null or undefined sources
5
+ if (source == null) continue
4
6
  merge(result, source)
5
7
  }
6
8
  return result
@@ -322,3 +322,19 @@ it('Safe Merge', () => {
322
322
  expect(mixed.__proto__).toEqual({})
323
323
  /* eslint-enable */
324
324
  })
325
+
326
+ it('should skip null and undefined sources', () => {
327
+ const obj1 = { a: 1 }
328
+ const obj2 = { b: 2 }
329
+ const expected = { a: 1, b: 2 }
330
+
331
+ expect(mix(obj1, null, obj2)).toEqual(expected)
332
+ expect(mix(null, obj1, obj2)).toEqual(expected)
333
+ expect(mix(obj1, obj2, null)).toEqual(expected)
334
+
335
+ expect(mix(obj1, undefined, obj2)).toEqual(expected)
336
+ expect(mix(undefined, obj1, obj2)).toEqual(expected)
337
+ expect(mix(obj1, obj2, undefined)).toEqual(expected)
338
+
339
+ expect(mix(null, obj1, undefined, obj2, null)).toEqual(expected)
340
+ })
@@ -34,3 +34,8 @@ export function getNestedProperty2(object, path) {
34
34
  const pathArray = path.split('.')
35
35
  return getNestedProperty(object, pathArray)
36
36
  }
37
+
38
+ // Same as dot syntax, but terser and more readable name
39
+ export function getNestedValue(object, path) {
40
+ return path.split('.').reduce((acc, part) => acc?.[part], object)
41
+ }
@@ -119,7 +119,7 @@ export async function getLastModifiedTime(globPath) {
119
119
  files = await Promise.all(files.map(fs.stat))
120
120
  files = files.map(file => file.mtimeMs)
121
121
 
122
- const sorted = sort(files, { sortBy: 'mtimeMs', order: 'desc' })
122
+ const sorted = sort(files, { props: 'mtimeMs', reverse: true })
123
123
  return sorted[0] || 0
124
124
  }
125
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splendidlabz/utils",
3
- "version": "1.5.0-alpha.1",
3
+ "version": "1.5.0-alpha.3",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,16 +23,16 @@
23
23
  "dependencies": {
24
24
  "dompurify": "^3.2.4",
25
25
  "glob-promise": "^6.0.7",
26
- "pluralize": "^8.0.0",
27
- "statuses": "^2.0.1",
28
26
  "marked": "^15.0.7",
29
27
  "marked-gfm-heading-id": "^4.1.1",
30
- "marked-mangle": "^1.1.10"
28
+ "marked-mangle": "^1.1.10",
29
+ "pluralize": "^8.0.0",
30
+ "statuses": "^2.0.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@splendidlabz/eslint-config": "*",
33
+ "@splendidlabz/eslint-config": "2.0.0-alpha.0",
34
34
  "jsdom": "^26.0.0",
35
35
  "np": "^10.2.0",
36
- "vitest": "^3.0.7"
36
+ "vitest": "^3.1.1"
37
37
  }
38
38
  }
package/.eslintrc.cjs DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- extends: ['@splendidlabz/eslint-config'],
3
- }
package/lib/index.test.js DELETED
@@ -1,9 +0,0 @@
1
- // Possible to test???
2
- import * as test from './index.js'
3
-
4
- import { expect, it } from 'vitest'
5
-
6
- it('Import Test', () => {
7
- console.log(test)
8
- console.log('hello')
9
- })