@pyreon/unistyle 0.11.1 → 0.11.2
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 +8 -7
- package/src/__tests__/alignContent.test.ts +121 -0
- package/src/__tests__/borderRadius.test.ts +125 -0
- package/src/__tests__/camelToKebab.test.ts +44 -0
- package/src/__tests__/context.test.ts +147 -0
- package/src/__tests__/createMediaQueries.test.ts +98 -0
- package/src/__tests__/edge.test.ts +164 -0
- package/src/__tests__/enrichTheme.test.ts +56 -0
- package/src/__tests__/extendCss.test.ts +45 -0
- package/src/__tests__/index.test.ts +79 -0
- package/src/__tests__/makeItResponsive.test.ts +171 -0
- package/src/__tests__/processDescriptor.test.ts +320 -0
- package/src/__tests__/responsive.test.ts +177 -0
- package/src/__tests__/styles.test.ts +119 -0
- package/src/__tests__/units.test.ts +134 -0
- package/src/context.tsx +34 -0
- package/src/enrichTheme.ts +42 -0
- package/src/index.ts +89 -0
- package/src/responsive/breakpoints.ts +15 -0
- package/src/responsive/createMediaQueries.ts +43 -0
- package/src/responsive/index.ts +14 -0
- package/src/responsive/makeItResponsive.ts +118 -0
- package/src/responsive/normalizeTheme.ts +65 -0
- package/src/responsive/optimizeTheme.ts +39 -0
- package/src/responsive/sortBreakpoints.ts +10 -0
- package/src/responsive/transformTheme.ts +48 -0
- package/src/styles/alignContent.ts +58 -0
- package/src/styles/extendCss.ts +26 -0
- package/src/styles/index.ts +16 -0
- package/src/styles/shorthands/borderRadius.ts +89 -0
- package/src/styles/shorthands/edge.ts +108 -0
- package/src/styles/shorthands/index.ts +4 -0
- package/src/styles/styles/camelToKebab.ts +3 -0
- package/src/styles/styles/index.ts +33 -0
- package/src/styles/styles/processDescriptor.ts +100 -0
- package/src/styles/styles/propertyMap.ts +436 -0
- package/src/styles/styles/types.ts +366 -0
- package/src/styles/styles/utils.ts +62 -0
- package/src/types.ts +175 -0
- package/src/units/index.ts +6 -0
- package/src/units/stripUnit.ts +25 -0
- package/src/units/value.ts +47 -0
- package/src/units/values.ts +40 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import stripUnit from "../units/stripUnit"
|
|
3
|
+
import value from "../units/value"
|
|
4
|
+
import values from "../units/values"
|
|
5
|
+
|
|
6
|
+
describe("stripUnit", () => {
|
|
7
|
+
it("strips px unit and returns number", () => {
|
|
8
|
+
expect(stripUnit("16px")).toBe(16)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("strips rem unit and returns number", () => {
|
|
12
|
+
expect(stripUnit("1.5rem")).toBe(1.5)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("strips em unit and returns number", () => {
|
|
16
|
+
expect(stripUnit("2em")).toBe(2)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("strips % and returns number", () => {
|
|
20
|
+
expect(stripUnit("50%")).toBe(50)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("handles negative values", () => {
|
|
24
|
+
expect(stripUnit("-10px")).toBe(-10)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("handles decimal values", () => {
|
|
28
|
+
expect(stripUnit("0.5rem")).toBe(0.5)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("returns original string for non-numeric strings", () => {
|
|
32
|
+
expect(stripUnit("auto")).toBe("auto")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("passes through numbers", () => {
|
|
36
|
+
expect(stripUnit(42)).toBe(42)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe("with unitReturn=true", () => {
|
|
40
|
+
it("returns [value, unit] tuple for px", () => {
|
|
41
|
+
expect(stripUnit("16px", true)).toEqual([16, "px"])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("returns [value, unit] tuple for rem", () => {
|
|
45
|
+
expect(stripUnit("2rem", true)).toEqual([2, "rem"])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("returns [value, unit] tuple for %", () => {
|
|
49
|
+
expect(stripUnit("50%", true)).toEqual([50, "%"])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("returns [value, unit] tuple for em", () => {
|
|
53
|
+
expect(stripUnit("1.5em", true)).toEqual([1.5, "em"])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("returns [value, empty string] for unitless number string", () => {
|
|
57
|
+
expect(stripUnit("42", true)).toEqual([42, ""])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("returns [number, undefined] for number input", () => {
|
|
61
|
+
expect(stripUnit(42, true)).toEqual([42, undefined])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("value", () => {
|
|
67
|
+
it("returns string values as-is", () => {
|
|
68
|
+
expect(value("50%")).toBe("50%")
|
|
69
|
+
expect(value("2em")).toBe("2em")
|
|
70
|
+
expect(value("100vh")).toBe("100vh")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("returns 0 as-is", () => {
|
|
74
|
+
expect(value(0)).toBe(0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("returns null for null/undefined", () => {
|
|
78
|
+
expect(value(null)).toBeNull()
|
|
79
|
+
expect(value(undefined)).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("converts unitless numbers to rem by default (divides by rootSize)", () => {
|
|
83
|
+
// 16 / 16 = 1rem
|
|
84
|
+
expect(value(16)).toBe("1rem")
|
|
85
|
+
// 32 / 16 = 2rem
|
|
86
|
+
expect(value(32)).toBe("2rem")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("converts px values to rem", () => {
|
|
90
|
+
expect(value("16px")).toBe("1rem")
|
|
91
|
+
expect(value("32px")).toBe("2rem")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("respects custom rootSize", () => {
|
|
95
|
+
// 20 / 10 = 2rem
|
|
96
|
+
expect(value(20, 10)).toBe("2rem")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("respects outputUnit=px for unitless numbers", () => {
|
|
100
|
+
expect(value(16, 16, "px")).toBe("16px")
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe("values", () => {
|
|
105
|
+
it("returns the first defined value converted", () => {
|
|
106
|
+
expect(values([undefined, null, 16])).toBe("1rem")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("returns the first value if defined", () => {
|
|
110
|
+
expect(values([32, 16])).toBe("2rem")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("passes through string values", () => {
|
|
114
|
+
expect(values(["50%"])).toBe("50%")
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("returns null when all values are null/undefined", () => {
|
|
118
|
+
expect(values([undefined, null])).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("returns 0 for zero value", () => {
|
|
122
|
+
expect(values([0])).toBe(0)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("joins array values with spaces", () => {
|
|
126
|
+
const result = values([[16, 32]])
|
|
127
|
+
expect(result).toContain("1rem")
|
|
128
|
+
expect(result).toContain("2rem")
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("respects rootSize parameter", () => {
|
|
132
|
+
expect(values([20], 10)).toBe("2rem")
|
|
133
|
+
})
|
|
134
|
+
})
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { provide } from "@pyreon/core"
|
|
3
|
+
import { ThemeContext } from "@pyreon/styler"
|
|
4
|
+
import { Provider as CoreProvider, context } from "@pyreon/ui-core"
|
|
5
|
+
import type { PyreonTheme } from "./enrichTheme"
|
|
6
|
+
import { enrichTheme } from "./enrichTheme"
|
|
7
|
+
|
|
8
|
+
export type TProvider = {
|
|
9
|
+
theme: PyreonTheme
|
|
10
|
+
children?: VNode | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Unistyle Provider — wraps the core Provider and enriches the theme
|
|
15
|
+
* with pre-computed sorted breakpoints and media-query tagged-template
|
|
16
|
+
* helpers consumed by `makeItResponsive`.
|
|
17
|
+
*/
|
|
18
|
+
function Provider(props: TProvider): VNode | null {
|
|
19
|
+
const { theme, children } = props
|
|
20
|
+
|
|
21
|
+
const enrichedTheme = enrichTheme(theme)
|
|
22
|
+
|
|
23
|
+
// Provide enriched theme to both the ui-core context (for rocketstyle/elements)
|
|
24
|
+
// AND the styler ThemeContext (for styled() components and makeItResponsive).
|
|
25
|
+
// Without this, styled() components receive an empty theme and all responsive
|
|
26
|
+
// styles are skipped (@media queries produce NaN values).
|
|
27
|
+
provide(ThemeContext, enrichedTheme)
|
|
28
|
+
|
|
29
|
+
return CoreProvider({ theme: enrichedTheme, children }) as VNode | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { context }
|
|
33
|
+
|
|
34
|
+
export default Provider
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { config, isEmpty } from "@pyreon/ui-core"
|
|
2
|
+
import { createMediaQueries, sortBreakpoints } from "./responsive"
|
|
3
|
+
|
|
4
|
+
export type PyreonTheme = {
|
|
5
|
+
rootSize?: number
|
|
6
|
+
breakpoints?: Record<string, number>
|
|
7
|
+
__PYREON__?: {
|
|
8
|
+
sortedBreakpoints: string[] | undefined
|
|
9
|
+
media: Record<string, (...args: any[]) => any> | undefined
|
|
10
|
+
}
|
|
11
|
+
} & Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enrich a theme with pre-computed responsive utilities.
|
|
15
|
+
* Adds sorted breakpoints and media-query tagged-template helpers
|
|
16
|
+
* to `theme.__PYREON__` for consumption by `makeItResponsive`.
|
|
17
|
+
*
|
|
18
|
+
* This is a pure function — safe to call outside of component context.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const enriched = enrichTheme({ rootSize: 16, breakpoints: { xs: 0, sm: 576, md: 768 } })
|
|
22
|
+
* enriched.__PYREON__.sortedBreakpoints // ['xs', 'sm', 'md']
|
|
23
|
+
* enriched.__PYREON__.media.sm // tagged-template for @media (min-width: 36em)
|
|
24
|
+
*/
|
|
25
|
+
export function enrichTheme<T extends PyreonTheme>(
|
|
26
|
+
theme: T,
|
|
27
|
+
): T & Required<Pick<PyreonTheme, "__PYREON__">> {
|
|
28
|
+
const { breakpoints, rootSize = 16 } = theme
|
|
29
|
+
|
|
30
|
+
const sortedBreakpoints =
|
|
31
|
+
breakpoints && !isEmpty(breakpoints) ? sortBreakpoints(breakpoints) : undefined
|
|
32
|
+
|
|
33
|
+
const media =
|
|
34
|
+
breakpoints && !isEmpty(breakpoints)
|
|
35
|
+
? createMediaQueries({ breakpoints, css: config.css, rootSize })
|
|
36
|
+
: undefined
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...theme,
|
|
40
|
+
__PYREON__: { sortedBreakpoints, media },
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { TProvider } from "./context"
|
|
2
|
+
import Provider, { context } from "./context"
|
|
3
|
+
import type { PyreonTheme } from "./enrichTheme"
|
|
4
|
+
import { enrichTheme } from "./enrichTheme"
|
|
5
|
+
import type {
|
|
6
|
+
Breakpoints,
|
|
7
|
+
CreateMediaQueries,
|
|
8
|
+
MakeItResponsive,
|
|
9
|
+
MakeItResponsiveStyles,
|
|
10
|
+
NormalizeTheme,
|
|
11
|
+
SortBreakpoints,
|
|
12
|
+
TransformTheme,
|
|
13
|
+
} from "./responsive"
|
|
14
|
+
import {
|
|
15
|
+
breakpoints,
|
|
16
|
+
createMediaQueries,
|
|
17
|
+
makeItResponsive,
|
|
18
|
+
normalizeTheme,
|
|
19
|
+
sortBreakpoints,
|
|
20
|
+
transformTheme,
|
|
21
|
+
} from "./responsive"
|
|
22
|
+
import type {
|
|
23
|
+
AlignContent,
|
|
24
|
+
AlignContentAlignXKeys,
|
|
25
|
+
AlignContentAlignYKeys,
|
|
26
|
+
AlignContentDirectionKeys,
|
|
27
|
+
ExtendCss,
|
|
28
|
+
Styles,
|
|
29
|
+
StylesTheme,
|
|
30
|
+
} from "./styles"
|
|
31
|
+
import {
|
|
32
|
+
ALIGN_CONTENT_DIRECTION,
|
|
33
|
+
ALIGN_CONTENT_MAP_X,
|
|
34
|
+
ALIGN_CONTENT_MAP_Y,
|
|
35
|
+
alignContent,
|
|
36
|
+
extendCss,
|
|
37
|
+
styles,
|
|
38
|
+
} from "./styles"
|
|
39
|
+
import type { BrowserColors, Color, Defaults, PropertyValue, UnitValue } from "./types"
|
|
40
|
+
import type { StripUnit, Value, Values } from "./units"
|
|
41
|
+
import { stripUnit, value, values } from "./units"
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
AlignContent,
|
|
45
|
+
AlignContentAlignXKeys,
|
|
46
|
+
AlignContentAlignYKeys,
|
|
47
|
+
AlignContentDirectionKeys,
|
|
48
|
+
Breakpoints,
|
|
49
|
+
BrowserColors,
|
|
50
|
+
Color,
|
|
51
|
+
CreateMediaQueries,
|
|
52
|
+
Defaults,
|
|
53
|
+
ExtendCss,
|
|
54
|
+
MakeItResponsive,
|
|
55
|
+
MakeItResponsiveStyles,
|
|
56
|
+
NormalizeTheme,
|
|
57
|
+
PropertyValue,
|
|
58
|
+
PyreonTheme,
|
|
59
|
+
SortBreakpoints,
|
|
60
|
+
StripUnit,
|
|
61
|
+
Styles,
|
|
62
|
+
StylesTheme,
|
|
63
|
+
TProvider,
|
|
64
|
+
TransformTheme,
|
|
65
|
+
UnitValue,
|
|
66
|
+
Value,
|
|
67
|
+
Values,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
ALIGN_CONTENT_DIRECTION,
|
|
72
|
+
ALIGN_CONTENT_MAP_X,
|
|
73
|
+
ALIGN_CONTENT_MAP_Y,
|
|
74
|
+
alignContent,
|
|
75
|
+
breakpoints,
|
|
76
|
+
context,
|
|
77
|
+
createMediaQueries,
|
|
78
|
+
enrichTheme,
|
|
79
|
+
extendCss,
|
|
80
|
+
makeItResponsive,
|
|
81
|
+
normalizeTheme,
|
|
82
|
+
Provider,
|
|
83
|
+
sortBreakpoints,
|
|
84
|
+
stripUnit,
|
|
85
|
+
styles,
|
|
86
|
+
transformTheme,
|
|
87
|
+
value,
|
|
88
|
+
values,
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type Css = (strings: TemplateStringsArray, ...values: any[]) => any
|
|
2
|
+
|
|
3
|
+
export type CreateMediaQueries = <
|
|
4
|
+
B extends Record<string, number>,
|
|
5
|
+
R extends number,
|
|
6
|
+
C extends Css,
|
|
7
|
+
>(props: {
|
|
8
|
+
breakpoints: B
|
|
9
|
+
rootSize: R
|
|
10
|
+
css: C
|
|
11
|
+
}) => Record<keyof B, (...args: any[]) => string>
|
|
12
|
+
|
|
13
|
+
// Implementation uses Record<string, ...> which is widened from Record<keyof B, ...>;
|
|
14
|
+
// the generic constraint on CreateMediaQueries ensures callers get the narrower type.
|
|
15
|
+
const createMediaQueries: CreateMediaQueries = ((props: {
|
|
16
|
+
breakpoints: Record<string, number>
|
|
17
|
+
rootSize: number
|
|
18
|
+
css: Css
|
|
19
|
+
}) => {
|
|
20
|
+
const { breakpoints, rootSize, css } = props
|
|
21
|
+
|
|
22
|
+
return Object.keys(breakpoints).reduce<
|
|
23
|
+
Record<string, (...args: [TemplateStringsArray, ...any[]]) => string>
|
|
24
|
+
>((acc, key) => {
|
|
25
|
+
const breakpointValue = breakpoints[key]
|
|
26
|
+
|
|
27
|
+
if (breakpointValue === 0) {
|
|
28
|
+
acc[key] = (...args: [TemplateStringsArray, ...any[]]) => css(...args)
|
|
29
|
+
} else if (breakpointValue != null) {
|
|
30
|
+
const emSize = breakpointValue / rootSize
|
|
31
|
+
|
|
32
|
+
acc[key] = (...args: [TemplateStringsArray, ...any[]]) => css`
|
|
33
|
+
@media only screen and (min-width: ${emSize}em) {
|
|
34
|
+
${css(...args)};
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return acc
|
|
40
|
+
}, {})
|
|
41
|
+
}) as CreateMediaQueries
|
|
42
|
+
|
|
43
|
+
export default createMediaQueries
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { Breakpoints } from "./breakpoints"
|
|
2
|
+
export { default as breakpoints } from "./breakpoints"
|
|
3
|
+
export type { CreateMediaQueries } from "./createMediaQueries"
|
|
4
|
+
export { default as createMediaQueries } from "./createMediaQueries"
|
|
5
|
+
export type { MakeItResponsive, MakeItResponsiveStyles } from "./makeItResponsive"
|
|
6
|
+
export { default as makeItResponsive } from "./makeItResponsive"
|
|
7
|
+
export type { NormalizeTheme } from "./normalizeTheme"
|
|
8
|
+
export { default as normalizeTheme } from "./normalizeTheme"
|
|
9
|
+
export type { OptimizeTheme } from "./optimizeTheme"
|
|
10
|
+
export { default as optimizeTheme } from "./optimizeTheme"
|
|
11
|
+
export type { SortBreakpoints } from "./sortBreakpoints"
|
|
12
|
+
export { default as sortBreakpoints } from "./sortBreakpoints"
|
|
13
|
+
export type { TransformTheme } from "./transformTheme"
|
|
14
|
+
export { default as transformTheme } from "./transformTheme"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isEmpty } from "@pyreon/ui-core"
|
|
2
|
+
import type createMediaQueries from "./createMediaQueries"
|
|
3
|
+
import normalizeTheme from "./normalizeTheme"
|
|
4
|
+
import optimizeTheme from "./optimizeTheme"
|
|
5
|
+
import type sortBreakpoints from "./sortBreakpoints"
|
|
6
|
+
import transformTheme from "./transformTheme"
|
|
7
|
+
|
|
8
|
+
type Css = (strings: TemplateStringsArray, ...values: any[]) => any
|
|
9
|
+
|
|
10
|
+
type CustomTheme = Record<string, Record<string, unknown> | number | string | boolean>
|
|
11
|
+
|
|
12
|
+
type Theme = Partial<{
|
|
13
|
+
rootSize: number
|
|
14
|
+
breakpoints: Record<string, number>
|
|
15
|
+
__PYREON__: Partial<{
|
|
16
|
+
media: ReturnType<typeof createMediaQueries>
|
|
17
|
+
sortedBreakpoints: ReturnType<typeof sortBreakpoints>
|
|
18
|
+
}>
|
|
19
|
+
}> &
|
|
20
|
+
CustomTheme
|
|
21
|
+
|
|
22
|
+
export type MakeItResponsiveStyles<T extends Partial<Record<string, any>> = any> = ({
|
|
23
|
+
theme,
|
|
24
|
+
css,
|
|
25
|
+
rootSize,
|
|
26
|
+
globalTheme,
|
|
27
|
+
}: {
|
|
28
|
+
theme: T
|
|
29
|
+
css: Css
|
|
30
|
+
rootSize?: number | undefined
|
|
31
|
+
globalTheme?: Record<string, any> | undefined
|
|
32
|
+
}) => ReturnType<typeof css> | string | any
|
|
33
|
+
|
|
34
|
+
export type MakeItResponsive = ({
|
|
35
|
+
theme,
|
|
36
|
+
key,
|
|
37
|
+
css,
|
|
38
|
+
styles,
|
|
39
|
+
normalize,
|
|
40
|
+
}: {
|
|
41
|
+
theme?: CustomTheme
|
|
42
|
+
key?: string
|
|
43
|
+
css: any
|
|
44
|
+
styles: MakeItResponsiveStyles
|
|
45
|
+
normalize?: boolean
|
|
46
|
+
}) => (props: { theme?: Theme; [prop: string]: any }) => any
|
|
47
|
+
|
|
48
|
+
const themeCache = new WeakMap<
|
|
49
|
+
object,
|
|
50
|
+
{ breakpoints: unknown; optimized: Record<string, Record<string, unknown>> }
|
|
51
|
+
>()
|
|
52
|
+
|
|
53
|
+
const makeItResponsive: MakeItResponsive =
|
|
54
|
+
({ theme: customTheme, key = "", css, styles, normalize = true }) =>
|
|
55
|
+
({ theme = {}, ...props }) => {
|
|
56
|
+
const internalTheme = customTheme || props[key]
|
|
57
|
+
|
|
58
|
+
if (isEmpty(internalTheme)) return ""
|
|
59
|
+
|
|
60
|
+
const { rootSize, breakpoints, __PYREON__, ...restTheme } = theme as Theme
|
|
61
|
+
|
|
62
|
+
const renderStyles = (styleTheme: Record<string, unknown>): ReturnType<typeof styles> =>
|
|
63
|
+
styles({ theme: styleTheme, css, rootSize, globalTheme: restTheme })
|
|
64
|
+
|
|
65
|
+
if (isEmpty(breakpoints) || isEmpty(__PYREON__)) {
|
|
66
|
+
return css`
|
|
67
|
+
${renderStyles(internalTheme)}
|
|
68
|
+
`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// isEmpty guard above ensures __PYREON__ is defined here
|
|
72
|
+
const { media, sortedBreakpoints } = __PYREON__ as NonNullable<typeof __PYREON__>
|
|
73
|
+
|
|
74
|
+
let optimizedTheme: Record<string, Record<string, unknown>>
|
|
75
|
+
|
|
76
|
+
const cached = themeCache.get(internalTheme)
|
|
77
|
+
if (cached && cached.breakpoints === sortedBreakpoints) {
|
|
78
|
+
optimizedTheme = cached.optimized
|
|
79
|
+
} else {
|
|
80
|
+
let helperTheme = internalTheme
|
|
81
|
+
|
|
82
|
+
if (normalize) {
|
|
83
|
+
helperTheme = normalizeTheme({
|
|
84
|
+
theme: internalTheme,
|
|
85
|
+
breakpoints: sortedBreakpoints,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const transformedTheme = transformTheme({
|
|
90
|
+
theme: helperTheme,
|
|
91
|
+
breakpoints: sortedBreakpoints,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
optimizedTheme = optimizeTheme({
|
|
95
|
+
theme: transformedTheme,
|
|
96
|
+
breakpoints: sortedBreakpoints,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
themeCache.set(internalTheme, {
|
|
100
|
+
breakpoints: sortedBreakpoints,
|
|
101
|
+
optimized: optimizedTheme,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return sortedBreakpoints.map((item: string) => {
|
|
106
|
+
const breakpointTheme = optimizedTheme[item]
|
|
107
|
+
|
|
108
|
+
if (!breakpointTheme || !media) return ""
|
|
109
|
+
|
|
110
|
+
const result = renderStyles(breakpointTheme)
|
|
111
|
+
|
|
112
|
+
return (media as Record<string, any>)[item]`
|
|
113
|
+
${result};
|
|
114
|
+
`
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default makeItResponsive
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type AssignToBreakpointKey = (
|
|
2
|
+
breakpoints: string[],
|
|
3
|
+
) => (
|
|
4
|
+
valueFn: (breakpoint: string, i: number, bps: string[], result: Record<string, unknown>) => void,
|
|
5
|
+
) => Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
const assignToBreakpointKey: AssignToBreakpointKey = (breakpoints) => (valueFn) => {
|
|
8
|
+
const result: Record<string, unknown> = {}
|
|
9
|
+
breakpoints.forEach((item, i) => {
|
|
10
|
+
result[item] = valueFn(item, i, breakpoints, result)
|
|
11
|
+
})
|
|
12
|
+
return result
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const handleArrayCb = (arr: (string | number)[]) => (_: unknown, i: number) => {
|
|
16
|
+
const currentValue = arr[i]
|
|
17
|
+
const lastValue = arr[arr.length - 1]
|
|
18
|
+
return currentValue ?? lastValue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handleObjectCb =
|
|
22
|
+
(obj: Record<string, unknown>) =>
|
|
23
|
+
(bp: string, i: number, bps: string[], res: Record<string, unknown>) => {
|
|
24
|
+
const currentValue = obj[bp]
|
|
25
|
+
const prevBp = bps[i - 1]
|
|
26
|
+
const previousValue = prevBp != null ? res[prevBp] : undefined
|
|
27
|
+
if (currentValue != null) return currentValue
|
|
28
|
+
return previousValue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleValueCb = (value: unknown) => () => value
|
|
32
|
+
|
|
33
|
+
const shouldNormalize = (props: Record<string, any>) =>
|
|
34
|
+
Object.values(props).some((item) => typeof item === "object" || Array.isArray(item))
|
|
35
|
+
|
|
36
|
+
export type NormalizeTheme = ({
|
|
37
|
+
theme,
|
|
38
|
+
breakpoints,
|
|
39
|
+
}: {
|
|
40
|
+
theme: Record<string, unknown>
|
|
41
|
+
breakpoints: string[]
|
|
42
|
+
}) => Record<string, unknown>
|
|
43
|
+
|
|
44
|
+
const normalizeTheme: NormalizeTheme = ({ theme, breakpoints }) => {
|
|
45
|
+
if (!shouldNormalize(theme)) return theme
|
|
46
|
+
|
|
47
|
+
const getBpValues = assignToBreakpointKey(breakpoints)
|
|
48
|
+
const result: Record<string, unknown> = {}
|
|
49
|
+
|
|
50
|
+
Object.entries(theme).forEach(([key, value]) => {
|
|
51
|
+
if (value == null) return
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
result[key] = getBpValues(handleArrayCb(value as (string | number)[]))
|
|
55
|
+
} else if (typeof value === "object") {
|
|
56
|
+
result[key] = getBpValues(handleObjectCb(value as Record<string, any>))
|
|
57
|
+
} else {
|
|
58
|
+
result[key] = getBpValues(handleValueCb(value))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default normalizeTheme
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type OptimizeTheme = ({
|
|
2
|
+
theme,
|
|
3
|
+
breakpoints,
|
|
4
|
+
}: {
|
|
5
|
+
theme: Record<string, Record<string, unknown>>
|
|
6
|
+
breakpoints: string[]
|
|
7
|
+
}) => Record<string, Record<string, unknown>>
|
|
8
|
+
|
|
9
|
+
const shallowEqual = (
|
|
10
|
+
a: Record<string, unknown> | undefined,
|
|
11
|
+
b: Record<string, unknown> | undefined,
|
|
12
|
+
): boolean => {
|
|
13
|
+
if (a === b) return true
|
|
14
|
+
if (!a || !b) return false
|
|
15
|
+
const keysA = Object.keys(a)
|
|
16
|
+
const keysB = Object.keys(b)
|
|
17
|
+
if (keysA.length !== keysB.length) return false
|
|
18
|
+
for (const key of keysA) {
|
|
19
|
+
if (a[key] !== b[key]) return false
|
|
20
|
+
}
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const optimizeTheme: OptimizeTheme = ({ theme, breakpoints }) => {
|
|
25
|
+
const result: Record<string, Record<string, unknown>> = {}
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < breakpoints.length; i++) {
|
|
28
|
+
const key = breakpoints[i] as string
|
|
29
|
+
const previousBreakpoint = breakpoints[i - 1] as string
|
|
30
|
+
const current = theme[key]
|
|
31
|
+
if (current && (i === 0 || !shallowEqual(theme[previousBreakpoint], current))) {
|
|
32
|
+
result[key] = current
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default optimizeTheme
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SortBreakpoints = <T extends Record<string, number>>(breakpoints: T) => (keyof T)[]
|
|
2
|
+
|
|
3
|
+
const sortBreakpoints: SortBreakpoints = (breakpoints) => {
|
|
4
|
+
const result = Object.keys(breakpoints).sort(
|
|
5
|
+
(a, b) => (breakpoints[a] ?? 0) - (breakpoints[b] ?? 0),
|
|
6
|
+
)
|
|
7
|
+
return result
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default sortBreakpoints
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isEmpty, set } from "@pyreon/ui-core"
|
|
2
|
+
|
|
3
|
+
const removeUnexpectedKeys = (obj: Record<string, unknown>, keys: string[]) => {
|
|
4
|
+
const result: Record<string, unknown> = {}
|
|
5
|
+
keys.forEach((bp) => {
|
|
6
|
+
const value = obj[bp]
|
|
7
|
+
if (value) {
|
|
8
|
+
result[bp] = value
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
return result
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TransformTheme = ({
|
|
15
|
+
theme,
|
|
16
|
+
breakpoints,
|
|
17
|
+
}: {
|
|
18
|
+
theme: Record<string, unknown>
|
|
19
|
+
breakpoints: string[]
|
|
20
|
+
}) => any
|
|
21
|
+
|
|
22
|
+
const transformTheme: TransformTheme = ({ theme, breakpoints }) => {
|
|
23
|
+
const result = {}
|
|
24
|
+
|
|
25
|
+
if (isEmpty(theme) || isEmpty(breakpoints)) return result
|
|
26
|
+
|
|
27
|
+
Object.entries(theme).forEach(([key, value]) => {
|
|
28
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
29
|
+
value.forEach((child, i) => {
|
|
30
|
+
const indexBreakpoint = breakpoints[i]
|
|
31
|
+
if (indexBreakpoint == null) return
|
|
32
|
+
set(result, [indexBreakpoint, key], child)
|
|
33
|
+
})
|
|
34
|
+
} else if (typeof value === "object" && value !== null) {
|
|
35
|
+
Object.entries(value).forEach(([childKey, childValue]) => {
|
|
36
|
+
set(result, [childKey, key], childValue)
|
|
37
|
+
})
|
|
38
|
+
} else if (value != null) {
|
|
39
|
+
const firstBreakpoint = breakpoints[0]
|
|
40
|
+
if (firstBreakpoint == null) return
|
|
41
|
+
set(result, [firstBreakpoint, key], value)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return removeUnexpectedKeys(result, breakpoints)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default transformTheme
|