-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
breakpoint.ts
130 lines (113 loc) · 3.74 KB
/
breakpoint.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { getLastItem } from "./array"
import { isNumber, isObject } from "./assertion"
import { fromEntries } from "./object"
import { Dict } from "./types"
function analyzeCSSValue(value: number | string) {
const num = parseFloat(value.toString())
const unit = value.toString().replace(String(num), "")
return { unitless: !unit, value: num, unit }
}
export function px(value: number | string | null): string | null {
if (value == null) return value as string | null
const { unitless } = analyzeCSSValue(value)
return unitless || isNumber(value) ? `${value}px` : value
}
const sortByBreakpointValue = (a: any[], b: any[]) =>
parseInt(a[1], 10) > parseInt(b[1], 10) ? 1 : -1
const sortBps = (breakpoints: Dict): Dict =>
fromEntries(Object.entries(breakpoints).sort(sortByBreakpointValue))
function normalize(breakpoints: Dict) {
const sorted = sortBps(breakpoints)
return Object.assign(Object.values(sorted), sorted) as string[]
}
function keys(breakpoints: Dict) {
const value = Object.keys(sortBps(breakpoints))
return new Set(value)
}
function subtract(value: string) {
if (!value) return value
value = px(value) ?? value
const factor = value.endsWith("px")
? -0.02
: // the equivalent of 0.02px in em using a 2px base
-0.01
return isNumber(value)
? `${value + factor}`
: value.replace(/(\d+\.?\d*)/u, (m) => `${parseFloat(m) + factor}`)
}
export function toMediaQueryString(min: string | null, max?: string) {
const query = ["@media screen"]
if (min) query.push("and", `(min-width: ${px(min)})`)
if (max) query.push("and", `(max-width: ${px(max)})`)
return query.join(" ")
}
export function analyzeBreakpoints(breakpoints: Dict) {
if (!breakpoints) return null
breakpoints.base = breakpoints.base ?? "0px"
const normalized = normalize(breakpoints)
const queries = Object.entries(breakpoints)
.sort(sortByBreakpointValue)
.map(([breakpoint, minW], index, entry) => {
let [, maxW] = entry[index + 1] ?? []
maxW = parseFloat(maxW) > 0 ? subtract(maxW) : undefined
return {
_minW: subtract(minW),
breakpoint,
minW,
maxW,
maxWQuery: toMediaQueryString(null, maxW),
minWQuery: toMediaQueryString(minW),
minMaxQuery: toMediaQueryString(minW, maxW),
}
})
const _keys = keys(breakpoints)
const _keysArr = Array.from(_keys.values())
return {
keys: _keys,
normalized,
isResponsive(test: Dict) {
const keys = Object.keys(test)
return keys.length > 0 && keys.every((key) => _keys.has(key))
},
asObject: sortBps(breakpoints),
asArray: normalize(breakpoints),
details: queries,
media: [
null,
...normalized.map((minW) => toMediaQueryString(minW)).slice(1),
],
/**
* Converts the object responsive syntax to array syntax
*
* @example
* toArrayValue({ base: 1, sm: 2, md: 3 }) // => [1, 2, 3]
*/
toArrayValue(test: Dict) {
if (!isObject(test)) {
throw new Error("toArrayValue: value must be an object")
}
const result = _keysArr.map((bp) => test[bp] ?? null)
while (getLastItem(result) === null) {
result.pop()
}
return result
},
/**
* Converts the array responsive syntax to object syntax
*
* @example
* toObjectValue([1, 2, 3]) // => { base: 1, sm: 2, md: 3 }
*/
toObjectValue(test: any[]) {
if (!Array.isArray(test)) {
throw new Error("toObjectValue: value must be an array")
}
return test.reduce((acc, value, index) => {
const key = _keysArr[index]
if (key != null && value != null) acc[key] = value
return acc
}, {} as Dict)
},
}
}
export type AnalyzeBreakpointsReturn = ReturnType<typeof analyzeBreakpoints>