@pyreon/coolgrid 0.11.0 → 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 +15 -13
- package/src/Col/component.tsx +61 -0
- package/src/Col/index.ts +3 -0
- package/src/Col/styled.ts +107 -0
- package/src/Container/component.tsx +82 -0
- package/src/Container/index.ts +3 -0
- package/src/Container/styled.ts +37 -0
- package/src/Container/utils.ts +13 -0
- package/src/Row/component.tsx +79 -0
- package/src/Row/index.ts +3 -0
- package/src/Row/styled.ts +70 -0
- package/src/__tests__/Col.test.ts +131 -0
- package/src/__tests__/Container.styled.test.ts +49 -0
- package/src/__tests__/Container.test.ts +147 -0
- package/src/__tests__/Row.test.ts +135 -0
- package/src/__tests__/config.test.ts +120 -0
- package/src/__tests__/contextCascading.test.ts +114 -0
- package/src/__tests__/index.test.ts +35 -0
- package/src/__tests__/useContext.test.ts +92 -0
- package/src/__tests__/utils.test.ts +144 -0
- package/src/constants.ts +20 -0
- package/src/context/ContainerContext.ts +9 -0
- package/src/context/RowContext.ts +9 -0
- package/src/context/index.ts +4 -0
- package/src/index.ts +7 -0
- package/src/theme.ts +40 -0
- package/src/types.ts +72 -0
- package/src/useContext.tsx +54 -0
- package/src/utils.ts +23 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { getContainerWidth } from "../Container/utils"
|
|
3
|
+
import { hasValue, hasWidth, isNumber, isVisible, omitCtxKeys } from "../utils"
|
|
4
|
+
|
|
5
|
+
describe("isNumber", () => {
|
|
6
|
+
it("returns true for finite numbers", () => {
|
|
7
|
+
expect(isNumber(0)).toBe(true)
|
|
8
|
+
expect(isNumber(1)).toBe(true)
|
|
9
|
+
expect(isNumber(-1)).toBe(true)
|
|
10
|
+
expect(isNumber(3.14)).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("returns false for non-finite values", () => {
|
|
14
|
+
expect(isNumber(Infinity)).toBe(false)
|
|
15
|
+
expect(isNumber(-Infinity)).toBe(false)
|
|
16
|
+
expect(isNumber(NaN)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("returns false for non-number types", () => {
|
|
20
|
+
expect(isNumber(null)).toBe(false)
|
|
21
|
+
expect(isNumber(undefined)).toBe(false)
|
|
22
|
+
expect(isNumber("5")).toBe(false)
|
|
23
|
+
expect(isNumber(true)).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("hasValue", () => {
|
|
28
|
+
it("returns true for positive finite numbers", () => {
|
|
29
|
+
expect(hasValue(1)).toBe(true)
|
|
30
|
+
expect(hasValue(12)).toBe(true)
|
|
31
|
+
expect(hasValue(0.5)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("returns false for zero", () => {
|
|
35
|
+
expect(hasValue(0)).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("returns false for negative numbers", () => {
|
|
39
|
+
expect(hasValue(-1)).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("returns false for non-numbers", () => {
|
|
43
|
+
expect(hasValue(null)).toBe(false)
|
|
44
|
+
expect(hasValue(undefined)).toBe(false)
|
|
45
|
+
expect(hasValue("5")).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("isVisible", () => {
|
|
50
|
+
it("returns true for positive numbers", () => {
|
|
51
|
+
expect(isVisible(1)).toBe(true)
|
|
52
|
+
expect(isVisible(12)).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("returns true for negative numbers", () => {
|
|
56
|
+
expect(isVisible(-1)).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("returns false for zero", () => {
|
|
60
|
+
expect(isVisible(0)).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("returns true for undefined (default visibility)", () => {
|
|
64
|
+
expect(isVisible(undefined)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("returns false for non-number truthy values", () => {
|
|
68
|
+
expect(isVisible("5")).toBe(false)
|
|
69
|
+
expect(isVisible(null)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("hasWidth", () => {
|
|
74
|
+
it("returns true when both size and columns are positive numbers", () => {
|
|
75
|
+
expect(hasWidth(6, 12)).toBe(true)
|
|
76
|
+
expect(hasWidth(1, 1)).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("returns false when size is 0", () => {
|
|
80
|
+
expect(hasWidth(0, 12)).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("returns false when columns is 0", () => {
|
|
84
|
+
expect(hasWidth(6, 0)).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("returns false when either is not a number", () => {
|
|
88
|
+
expect(hasWidth(null, 12)).toBe(false)
|
|
89
|
+
expect(hasWidth(6, null)).toBe(false)
|
|
90
|
+
expect(hasWidth(undefined, undefined)).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("omitCtxKeys", () => {
|
|
95
|
+
it("strips context keys from props", () => {
|
|
96
|
+
const props = {
|
|
97
|
+
columns: 12,
|
|
98
|
+
size: 6,
|
|
99
|
+
gap: 16,
|
|
100
|
+
padding: 4,
|
|
101
|
+
gutter: 8,
|
|
102
|
+
colCss: "color: red;",
|
|
103
|
+
colComponent: () => null,
|
|
104
|
+
rowCss: "color: blue;",
|
|
105
|
+
rowComponent: () => null,
|
|
106
|
+
contentAlignX: "center",
|
|
107
|
+
className: "my-class",
|
|
108
|
+
id: "my-id",
|
|
109
|
+
}
|
|
110
|
+
const result = omitCtxKeys(props)
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
className: "my-class",
|
|
113
|
+
id: "my-id",
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("returns all props when no context keys present", () => {
|
|
118
|
+
const props = { className: "test", style: "color: red;" }
|
|
119
|
+
const result = omitCtxKeys(props)
|
|
120
|
+
expect(result).toEqual(props)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("getContainerWidth", () => {
|
|
125
|
+
it("returns width from props", () => {
|
|
126
|
+
const result = getContainerWidth({ width: { xs: 600 } }, {})
|
|
127
|
+
expect(result).toEqual({ xs: 600 })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("falls back to theme.grid.container", () => {
|
|
131
|
+
const result = getContainerWidth({}, { grid: { container: { xs: "100%" } } })
|
|
132
|
+
expect(result).toEqual({ xs: "100%" })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("falls back to theme.coolgrid.container", () => {
|
|
136
|
+
const result = getContainerWidth({}, { coolgrid: { container: { md: 720 } } })
|
|
137
|
+
expect(result).toEqual({ md: 720 })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("returns undefined when nothing matches", () => {
|
|
141
|
+
const result = getContainerWidth({}, {})
|
|
142
|
+
expect(result).toBeFalsy()
|
|
143
|
+
})
|
|
144
|
+
})
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const PKG_NAME = "@pyreon/coolgrid"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Grid configuration keys that are passed through context
|
|
5
|
+
* from Container to Row and from Row to Col components.
|
|
6
|
+
*/
|
|
7
|
+
export const CONTEXT_KEYS = [
|
|
8
|
+
// 'breakpoints',
|
|
9
|
+
// 'rootSize',
|
|
10
|
+
"columns",
|
|
11
|
+
"size",
|
|
12
|
+
"gap",
|
|
13
|
+
"padding",
|
|
14
|
+
"gutter",
|
|
15
|
+
"colCss",
|
|
16
|
+
"colComponent",
|
|
17
|
+
"rowCss",
|
|
18
|
+
"rowComponent",
|
|
19
|
+
"contentAlignX",
|
|
20
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext } from "@pyreon/core"
|
|
2
|
+
import type { Context as ContextType } from "../types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context for container-level grid configuration.
|
|
6
|
+
* Provided by the Container component and consumed by Row children
|
|
7
|
+
* to inherit columns, gap, gutter, and other grid settings.
|
|
8
|
+
*/
|
|
9
|
+
export default createContext<ContextType>({})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext } from "@pyreon/core"
|
|
2
|
+
import type { Context as ContextType } from "../types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context for row-level grid configuration.
|
|
6
|
+
* Provided by the Row component and consumed by Col children
|
|
7
|
+
* to inherit columns, gap, gutter, and sizing for width calculations.
|
|
8
|
+
*/
|
|
9
|
+
export default createContext<ContextType>({})
|
package/src/index.ts
ADDED
package/src/theme.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Bootstrap-like grid configuration. Provides 5 breakpoints (xs-xl),
|
|
3
|
+
* a 12-column grid, and responsive container max-widths matching Bootstrap 4.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
rootSize: 16,
|
|
7
|
+
breakpoints: {
|
|
8
|
+
xs: 0,
|
|
9
|
+
sm: 576,
|
|
10
|
+
md: 768,
|
|
11
|
+
lg: 992,
|
|
12
|
+
xl: 1200,
|
|
13
|
+
},
|
|
14
|
+
grid: {
|
|
15
|
+
columns: 12,
|
|
16
|
+
container: {
|
|
17
|
+
xs: "100%",
|
|
18
|
+
sm: 540,
|
|
19
|
+
md: 720,
|
|
20
|
+
lg: 960,
|
|
21
|
+
xl: 1140,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export const defaultBreakpoints: Record<string, number> = {
|
|
27
|
+
xs: 0,
|
|
28
|
+
sm: 576,
|
|
29
|
+
md: 768,
|
|
30
|
+
lg: 992,
|
|
31
|
+
xl: 1200,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const defaultContainerWidths: Record<string, string | number> = {
|
|
35
|
+
xs: "100%",
|
|
36
|
+
sm: 540,
|
|
37
|
+
md: 720,
|
|
38
|
+
lg: 960,
|
|
39
|
+
xl: 1140,
|
|
40
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the coolgrid layout system.
|
|
3
|
+
* Supports responsive values as single values, arrays (mobile-first),
|
|
4
|
+
* or breakpoint-keyed objects. Defines config props for Container/Row/Col
|
|
5
|
+
* and the resolved styled-component prop types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ComponentFn, VNodeChild } from "@pyreon/core"
|
|
9
|
+
import type { BreakpointKeys, config } from "@pyreon/ui-core"
|
|
10
|
+
import type { AlignContentAlignXKeys, extendCss } from "@pyreon/unistyle"
|
|
11
|
+
|
|
12
|
+
type CreateValueType<T> = T | T[] | Partial<Record<BreakpointKeys, T>>
|
|
13
|
+
|
|
14
|
+
export type Obj = Record<string, unknown>
|
|
15
|
+
export type Value = string | number
|
|
16
|
+
export type Css = Parameters<typeof extendCss>[0]
|
|
17
|
+
export type ExtraStyles = CreateValueType<Css>
|
|
18
|
+
|
|
19
|
+
export type CssOutput = ReturnType<typeof config.css> | string
|
|
20
|
+
|
|
21
|
+
export type ValueType = CreateValueType<number>
|
|
22
|
+
export type ContainerWidth = CreateValueType<Value>
|
|
23
|
+
|
|
24
|
+
export type ContentAlignX =
|
|
25
|
+
| "center"
|
|
26
|
+
| "left"
|
|
27
|
+
| "right"
|
|
28
|
+
| "spaceAround"
|
|
29
|
+
| "spaceBetween"
|
|
30
|
+
| "spaceEvenly"
|
|
31
|
+
|
|
32
|
+
export type ConfigurationProps = Partial<{
|
|
33
|
+
size: ValueType
|
|
34
|
+
padding: ValueType
|
|
35
|
+
gap: ValueType
|
|
36
|
+
gutter: ValueType
|
|
37
|
+
columns: ValueType
|
|
38
|
+
colCss: ExtraStyles
|
|
39
|
+
rowCss: ExtraStyles
|
|
40
|
+
colComponent: ComponentFn<any>
|
|
41
|
+
rowComponent: ComponentFn<any>
|
|
42
|
+
contentAlignX: ContentAlignX
|
|
43
|
+
containerWidth: ContainerWidth
|
|
44
|
+
width: ContainerWidth | ((widths: Record<string, any>) => ContainerWidth)
|
|
45
|
+
}>
|
|
46
|
+
|
|
47
|
+
export type ComponentProps = ConfigurationProps &
|
|
48
|
+
Partial<{
|
|
49
|
+
component: ComponentFn<any>
|
|
50
|
+
css: ExtraStyles
|
|
51
|
+
}>
|
|
52
|
+
|
|
53
|
+
export type StyledTypes = Partial<{
|
|
54
|
+
size: number | undefined
|
|
55
|
+
padding: number | undefined
|
|
56
|
+
gap: number | undefined
|
|
57
|
+
gutter: number | undefined
|
|
58
|
+
columns: number | undefined
|
|
59
|
+
extraStyles: Css
|
|
60
|
+
contentAlignX: AlignContentAlignXKeys
|
|
61
|
+
width: Value
|
|
62
|
+
}>
|
|
63
|
+
|
|
64
|
+
export type ElementType<O extends string[]> = ComponentFn<
|
|
65
|
+
Omit<ComponentProps, O[number]> & Record<string, unknown> & { children?: VNodeChild }
|
|
66
|
+
> & {
|
|
67
|
+
displayName: string
|
|
68
|
+
pkgName: string
|
|
69
|
+
PYREON__COMPONENT: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type Context = { [K in keyof ConfigurationProps]?: ConfigurationProps[K] | undefined }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useContext } from "@pyreon/core"
|
|
2
|
+
import { get, pick } from "@pyreon/ui-core"
|
|
3
|
+
import { context } from "@pyreon/unistyle"
|
|
4
|
+
import { CONTEXT_KEYS } from "./constants"
|
|
5
|
+
import type { Context, Obj, ValueType } from "./types"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Picks only the recognized grid configuration keys from a props object,
|
|
9
|
+
* filtering out any non-grid props before they enter context resolution.
|
|
10
|
+
*/
|
|
11
|
+
export type PickThemeProps = <T extends Record<string, unknown>>(
|
|
12
|
+
props: T,
|
|
13
|
+
keywords: Array<keyof T>,
|
|
14
|
+
) => ReturnType<typeof pick>
|
|
15
|
+
const pickThemeProps: PickThemeProps = (props, keywords) => pick(props, keywords)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves grid columns and container width using a three-layer fallback:
|
|
19
|
+
* 1. Explicit component props (e.g. `columns={6}`)
|
|
20
|
+
* 2. `theme.grid.columns` / `theme.grid.container`
|
|
21
|
+
* 3. `theme.coolgrid.columns` / `theme.coolgrid.container`
|
|
22
|
+
*/
|
|
23
|
+
type GetGridContext = (
|
|
24
|
+
props: Obj,
|
|
25
|
+
theme: Obj,
|
|
26
|
+
) => {
|
|
27
|
+
columns?: ValueType
|
|
28
|
+
containerWidth?: Record<string, number>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getGridContext: GetGridContext = (props = {}, theme = {}) => ({
|
|
32
|
+
columns: (get(props, "columns") ||
|
|
33
|
+
get(theme, "grid.columns") ||
|
|
34
|
+
get(theme, "coolgrid.columns")) as ValueType,
|
|
35
|
+
containerWidth: (get(props, "width") ||
|
|
36
|
+
get(theme, "grid.container") ||
|
|
37
|
+
get(theme, "coolgrid.container")) as Record<string, number>,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hook that reads the unistyle theme context and merges it with the
|
|
42
|
+
* component's own props to produce the final grid configuration.
|
|
43
|
+
* Applies the three-layer resolution (props -> grid.* -> coolgrid.*).
|
|
44
|
+
*/
|
|
45
|
+
type UseGridContext = (props: Obj) => Context
|
|
46
|
+
const useGridContext: UseGridContext = (props) => {
|
|
47
|
+
const { theme } = useContext(context)
|
|
48
|
+
const ctxProps = pickThemeProps(props, CONTEXT_KEYS)
|
|
49
|
+
const gridContext = getGridContext(ctxProps, theme as Record<string, unknown>)
|
|
50
|
+
|
|
51
|
+
return { ...gridContext, ...ctxProps }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default useGridContext
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { omit } from "@pyreon/ui-core"
|
|
2
|
+
import { CONTEXT_KEYS } from "./constants"
|
|
3
|
+
|
|
4
|
+
/** Checks whether a value is a finite number. */
|
|
5
|
+
export const isNumber = (value: unknown): value is number => Number.isFinite(value)
|
|
6
|
+
|
|
7
|
+
/** Checks whether a value is a finite number greater than zero. */
|
|
8
|
+
export const hasValue = (value: unknown): boolean => isNumber(value) && value > 0
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Determines if a column should be visible. A column is visible when its
|
|
12
|
+
* size is undefined (auto) or a non-zero number. Size 0 hides the column.
|
|
13
|
+
*/
|
|
14
|
+
export const isVisible = (value: unknown): boolean =>
|
|
15
|
+
(isNumber(value) && value !== 0) || value === undefined
|
|
16
|
+
|
|
17
|
+
/** Returns true when both size and columns are positive numbers, indicating an explicit width can be calculated. */
|
|
18
|
+
type HasWidth = (size: unknown, columns: unknown) => boolean
|
|
19
|
+
export const hasWidth: HasWidth = (size, columns) => !!(hasValue(size) && hasValue(columns))
|
|
20
|
+
|
|
21
|
+
/** Strips grid context keys from a props object so they are not forwarded to the DOM element. */
|
|
22
|
+
type OmitCtxKeys = (props?: Record<string, any>) => ReturnType<typeof omit>
|
|
23
|
+
export const omitCtxKeys: OmitCtxKeys = (props) => omit(props, CONTEXT_KEYS)
|