@pyreon/unistyle 0.11.1 → 0.11.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.
Files changed (43) hide show
  1. package/package.json +8 -7
  2. package/src/__tests__/alignContent.test.ts +121 -0
  3. package/src/__tests__/borderRadius.test.ts +125 -0
  4. package/src/__tests__/camelToKebab.test.ts +44 -0
  5. package/src/__tests__/context.test.ts +147 -0
  6. package/src/__tests__/createMediaQueries.test.ts +98 -0
  7. package/src/__tests__/edge.test.ts +164 -0
  8. package/src/__tests__/enrichTheme.test.ts +56 -0
  9. package/src/__tests__/extendCss.test.ts +45 -0
  10. package/src/__tests__/index.test.ts +79 -0
  11. package/src/__tests__/makeItResponsive.test.ts +171 -0
  12. package/src/__tests__/processDescriptor.test.ts +320 -0
  13. package/src/__tests__/responsive.test.ts +177 -0
  14. package/src/__tests__/styles.test.ts +119 -0
  15. package/src/__tests__/units.test.ts +134 -0
  16. package/src/context.tsx +34 -0
  17. package/src/enrichTheme.ts +42 -0
  18. package/src/index.ts +89 -0
  19. package/src/responsive/breakpoints.ts +15 -0
  20. package/src/responsive/createMediaQueries.ts +43 -0
  21. package/src/responsive/index.ts +14 -0
  22. package/src/responsive/makeItResponsive.ts +118 -0
  23. package/src/responsive/normalizeTheme.ts +65 -0
  24. package/src/responsive/optimizeTheme.ts +39 -0
  25. package/src/responsive/sortBreakpoints.ts +10 -0
  26. package/src/responsive/transformTheme.ts +48 -0
  27. package/src/styles/alignContent.ts +58 -0
  28. package/src/styles/extendCss.ts +26 -0
  29. package/src/styles/index.ts +16 -0
  30. package/src/styles/shorthands/borderRadius.ts +89 -0
  31. package/src/styles/shorthands/edge.ts +108 -0
  32. package/src/styles/shorthands/index.ts +4 -0
  33. package/src/styles/styles/camelToKebab.ts +3 -0
  34. package/src/styles/styles/index.ts +33 -0
  35. package/src/styles/styles/processDescriptor.ts +100 -0
  36. package/src/styles/styles/propertyMap.ts +436 -0
  37. package/src/styles/styles/types.ts +366 -0
  38. package/src/styles/styles/utils.ts +62 -0
  39. package/src/types.ts +175 -0
  40. package/src/units/index.ts +6 -0
  41. package/src/units/stripUnit.ts +25 -0
  42. package/src/units/value.ts +47 -0
  43. 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
+ })
@@ -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,15 @@
1
+ const breakpoints = {
2
+ rootSize: 16,
3
+ breakpoints: {
4
+ xs: 0,
5
+ sm: 576,
6
+ md: 768,
7
+ lg: 992,
8
+ xl: 1200,
9
+ xxl: 1440,
10
+ },
11
+ } as const
12
+
13
+ export type Breakpoints = typeof breakpoints
14
+
15
+ export default breakpoints
@@ -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