@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
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/coolgrid",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
|
-
"url": "https://github.com/pyreon/
|
|
6
|
+
"url": "https://github.com/pyreon/pyreon",
|
|
7
7
|
"directory": "packages/ui-system/coolgrid"
|
|
8
8
|
},
|
|
9
9
|
"description": "Responsive grid system for Pyreon",
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"type": "module",
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"exports": {
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
".": {
|
|
15
|
+
"bun": "./src/index.ts",
|
|
16
|
+
"import": "./lib/index.js",
|
|
17
|
+
"types": "./lib/index.d.ts"
|
|
18
|
+
}
|
|
18
19
|
},
|
|
19
20
|
"types": "./lib/index.d.ts",
|
|
20
21
|
"main": "./lib/index.js",
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
"!lib/**/*.map",
|
|
24
25
|
"!lib/analysis",
|
|
25
26
|
"README.md",
|
|
26
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"src"
|
|
27
29
|
],
|
|
28
30
|
"engines": {
|
|
29
31
|
"node": ">= 22"
|
|
@@ -42,14 +44,14 @@
|
|
|
42
44
|
"typecheck": "tsc --noEmit"
|
|
43
45
|
},
|
|
44
46
|
"peerDependencies": {
|
|
45
|
-
"@pyreon/core": "^0.11.
|
|
46
|
-
"@pyreon/reactivity": "^0.11.
|
|
47
|
-
"@pyreon/ui-core": "^0.11.
|
|
48
|
-
"@pyreon/unistyle": "^0.11.
|
|
49
|
-
"@pyreon/styler": "^0.11.
|
|
47
|
+
"@pyreon/core": "^0.11.2",
|
|
48
|
+
"@pyreon/reactivity": "^0.11.2",
|
|
49
|
+
"@pyreon/ui-core": "^0.11.2",
|
|
50
|
+
"@pyreon/unistyle": "^0.11.2",
|
|
51
|
+
"@pyreon/styler": "^0.11.2"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@vitus-labs/tools-rolldown": "^1.15.3",
|
|
53
|
-
"@pyreon/typescript": "^0.11.
|
|
55
|
+
"@pyreon/typescript": "^0.11.2"
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useContext } from "@pyreon/core"
|
|
2
|
+
import { PKG_NAME } from "../constants"
|
|
3
|
+
import { RowContext } from "../context"
|
|
4
|
+
import type { ElementType } from "../types"
|
|
5
|
+
import useGridContext from "../useContext"
|
|
6
|
+
import { omitCtxKeys } from "../utils"
|
|
7
|
+
import Styled from "./styled"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Col (column) component that reads grid settings from RowContext
|
|
11
|
+
* (columns, gap, gutter) and calculates its own width as a fraction
|
|
12
|
+
* of the total columns. Supports responsive size, padding, and visibility.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEV_PROPS: Record<string, string> =
|
|
16
|
+
process.env.NODE_ENV !== "production" ? { "data-coolgrid": "col" } : {}
|
|
17
|
+
|
|
18
|
+
const Component: ElementType<
|
|
19
|
+
[
|
|
20
|
+
"containerWidth",
|
|
21
|
+
"width",
|
|
22
|
+
"rowComponent",
|
|
23
|
+
"rowCss",
|
|
24
|
+
"colCss",
|
|
25
|
+
"colComponent",
|
|
26
|
+
"columns",
|
|
27
|
+
"gap",
|
|
28
|
+
"gutter",
|
|
29
|
+
"contentAlignX",
|
|
30
|
+
]
|
|
31
|
+
> = ({ children, component, css, ...props }) => {
|
|
32
|
+
const parentCtx = useContext(RowContext)
|
|
33
|
+
const { colCss, colComponent, columns, gap, size, padding } = useGridContext({
|
|
34
|
+
...parentCtx,
|
|
35
|
+
...props,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const finalProps = {
|
|
39
|
+
$coolgrid: {
|
|
40
|
+
columns,
|
|
41
|
+
gap,
|
|
42
|
+
size,
|
|
43
|
+
padding,
|
|
44
|
+
extraStyles: css ?? colCss,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Styled {...omitCtxKeys(props)} as={component ?? colComponent} {...finalProps} {...DEV_PROPS}>
|
|
50
|
+
{children}
|
|
51
|
+
</Styled>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const name = `${PKG_NAME}/Col`
|
|
56
|
+
|
|
57
|
+
Component.displayName = name
|
|
58
|
+
Component.pkgName = PKG_NAME
|
|
59
|
+
Component.PYREON__COMPONENT = name
|
|
60
|
+
|
|
61
|
+
export default Component
|
package/src/Col/index.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { config } from "@pyreon/ui-core"
|
|
2
|
+
import type { MakeItResponsiveStyles } from "@pyreon/unistyle"
|
|
3
|
+
import { extendCss, makeItResponsive, value } from "@pyreon/unistyle"
|
|
4
|
+
import type { CssOutput, StyledTypes } from "../types"
|
|
5
|
+
import { hasValue, isNumber, isVisible } from "../utils"
|
|
6
|
+
|
|
7
|
+
const { styled, css, component } = config
|
|
8
|
+
|
|
9
|
+
type HasWidth = (size?: number, columns?: number) => boolean
|
|
10
|
+
|
|
11
|
+
/** Returns true when both size and columns are valid, enabling explicit width calculation. */
|
|
12
|
+
const hasWidth: HasWidth = (size, columns) => hasValue(size) && hasValue(columns)
|
|
13
|
+
|
|
14
|
+
type WidthStyles = (
|
|
15
|
+
props: Pick<StyledTypes, "size" | "columns" | "gap">,
|
|
16
|
+
defaults: { rootSize?: number | undefined },
|
|
17
|
+
) => CssOutput
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculates column width as a percentage of total columns, subtracting
|
|
21
|
+
* the gap when present. Uses `calc(%)` for web.
|
|
22
|
+
*/
|
|
23
|
+
const widthStyles: WidthStyles = ({ size, columns, gap }, { rootSize }) => {
|
|
24
|
+
if (!hasWidth(size, columns)) {
|
|
25
|
+
return ""
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const s = size as number
|
|
29
|
+
const c = columns as number
|
|
30
|
+
const g = gap as number
|
|
31
|
+
|
|
32
|
+
// calculate % of width
|
|
33
|
+
const width = (s / c) * 100
|
|
34
|
+
|
|
35
|
+
const hasGap = hasValue(gap)
|
|
36
|
+
|
|
37
|
+
const val = hasGap ? `calc(${width}% - ${g}px)` : `${width}%`
|
|
38
|
+
|
|
39
|
+
const v = value(val, rootSize)
|
|
40
|
+
|
|
41
|
+
return css`
|
|
42
|
+
flex-grow: 0;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
max-width: ${v};
|
|
45
|
+
flex-basis: ${v};
|
|
46
|
+
`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type SpacingStyles = (type: "margin" | "padding", param?: number, rootSize?: number) => CssOutput
|
|
50
|
+
/** Applies half of the given value as either margin or padding (used for gap and padding distribution). */
|
|
51
|
+
const spacingStyles: SpacingStyles = (type, param, rootSize) => {
|
|
52
|
+
if (!isNumber(param)) {
|
|
53
|
+
return ""
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const finalStyle = `${type}: ${value((param as number) / 2, rootSize)}`
|
|
57
|
+
|
|
58
|
+
return css`
|
|
59
|
+
${finalStyle};
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main responsive style block for Col. When the column is visible, applies
|
|
65
|
+
* width, padding, margin, and extra CSS. When hidden (size === 0), moves
|
|
66
|
+
* the element off-screen with fixed positioning.
|
|
67
|
+
*/
|
|
68
|
+
const styles: MakeItResponsiveStyles<StyledTypes> = ({ theme, css: cssFn, rootSize }) => {
|
|
69
|
+
const { size, columns, gap, padding, extraStyles } = theme
|
|
70
|
+
const renderStyles = isVisible(size)
|
|
71
|
+
|
|
72
|
+
if (renderStyles) {
|
|
73
|
+
return cssFn`
|
|
74
|
+
left: initial;
|
|
75
|
+
position: relative;
|
|
76
|
+
${widthStyles({ size, columns, gap }, { rootSize })};
|
|
77
|
+
${spacingStyles("padding", padding, rootSize)};
|
|
78
|
+
${spacingStyles("margin", gap, rootSize)};
|
|
79
|
+
${extendCss(extraStyles)};
|
|
80
|
+
`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return cssFn`
|
|
84
|
+
left: -9999px;
|
|
85
|
+
position: fixed;
|
|
86
|
+
margin: 0;
|
|
87
|
+
padding: 0;
|
|
88
|
+
`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default styled(component)`
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
justify-content: stretch;
|
|
94
|
+
|
|
95
|
+
position: relative;
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-basis: 0;
|
|
98
|
+
flex-grow: 1;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
|
|
101
|
+
${makeItResponsive({
|
|
102
|
+
key: "$coolgrid",
|
|
103
|
+
styles,
|
|
104
|
+
css,
|
|
105
|
+
normalize: true,
|
|
106
|
+
})};
|
|
107
|
+
`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { provide } from "@pyreon/core"
|
|
2
|
+
import { PKG_NAME } from "../constants"
|
|
3
|
+
import ContainerContext from "../context/ContainerContext"
|
|
4
|
+
import type { ElementType } from "../types"
|
|
5
|
+
import useGridContext from "../useContext"
|
|
6
|
+
import { omitCtxKeys } from "../utils"
|
|
7
|
+
import Styled from "./styled"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Container component that establishes the outermost grid boundary.
|
|
11
|
+
* Resolves grid config from the theme, provides it to descendant Row/Col
|
|
12
|
+
* components via ContainerContext, and renders a styled wrapper with
|
|
13
|
+
* responsive max-width.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEV_PROPS: Record<string, string> =
|
|
17
|
+
process.env.NODE_ENV !== "production" ? { "data-coolgrid": "container" } : {}
|
|
18
|
+
|
|
19
|
+
const Component: ElementType<["containerWidth"]> = ({
|
|
20
|
+
children,
|
|
21
|
+
component,
|
|
22
|
+
css,
|
|
23
|
+
width,
|
|
24
|
+
...props
|
|
25
|
+
}) => {
|
|
26
|
+
const {
|
|
27
|
+
containerWidth,
|
|
28
|
+
columns,
|
|
29
|
+
size,
|
|
30
|
+
gap,
|
|
31
|
+
padding,
|
|
32
|
+
gutter,
|
|
33
|
+
colCss,
|
|
34
|
+
colComponent,
|
|
35
|
+
rowCss,
|
|
36
|
+
rowComponent,
|
|
37
|
+
contentAlignX,
|
|
38
|
+
} = useGridContext(props)
|
|
39
|
+
|
|
40
|
+
const context = {
|
|
41
|
+
columns,
|
|
42
|
+
size,
|
|
43
|
+
gap,
|
|
44
|
+
padding,
|
|
45
|
+
gutter,
|
|
46
|
+
colCss,
|
|
47
|
+
colComponent,
|
|
48
|
+
rowCss,
|
|
49
|
+
rowComponent,
|
|
50
|
+
contentAlignX,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const finalWidth = (() => {
|
|
54
|
+
if (!width) return containerWidth
|
|
55
|
+
if (typeof width === "function") return width(containerWidth as Record<string, any>)
|
|
56
|
+
return width
|
|
57
|
+
})()
|
|
58
|
+
|
|
59
|
+
const finalProps = {
|
|
60
|
+
$coolgrid: {
|
|
61
|
+
width: finalWidth,
|
|
62
|
+
extraStyles: css,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Provide container context to descendant Row/Col components
|
|
67
|
+
provide(ContainerContext, context)
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Styled {...omitCtxKeys(props)} as={component} {...finalProps} {...DEV_PROPS}>
|
|
71
|
+
{children}
|
|
72
|
+
</Styled>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const name = `${PKG_NAME}/Container`
|
|
77
|
+
|
|
78
|
+
Component.displayName = name
|
|
79
|
+
Component.pkgName = PKG_NAME
|
|
80
|
+
Component.PYREON__COMPONENT = name
|
|
81
|
+
|
|
82
|
+
export default Component
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { config } from "@pyreon/ui-core"
|
|
2
|
+
import type { MakeItResponsiveStyles } from "@pyreon/unistyle"
|
|
3
|
+
import { extendCss, makeItResponsive, value } from "@pyreon/unistyle"
|
|
4
|
+
import type { StyledTypes } from "../types"
|
|
5
|
+
|
|
6
|
+
const { styled, css, component } = config
|
|
7
|
+
|
|
8
|
+
/** Responsive styles that apply the container's max-width and any extra CSS at each breakpoint. */
|
|
9
|
+
const styles: MakeItResponsiveStyles<Pick<StyledTypes, "width" | "extraStyles">> = ({
|
|
10
|
+
theme: t,
|
|
11
|
+
css: cssFn,
|
|
12
|
+
rootSize,
|
|
13
|
+
}) => {
|
|
14
|
+
const w = t.width != null && typeof t.width !== "object" ? t.width : null
|
|
15
|
+
|
|
16
|
+
return cssFn`
|
|
17
|
+
${w != null ? `max-width: ${value(w, rootSize)};` : ""};
|
|
18
|
+
${extendCss(t.extraStyles)};
|
|
19
|
+
`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Styled Container element. Centered via auto margins with responsive max-width. */
|
|
23
|
+
export default styled(component)`
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
width: 100%;
|
|
28
|
+
margin-right: auto;
|
|
29
|
+
margin-left: auto;
|
|
30
|
+
|
|
31
|
+
${makeItResponsive({
|
|
32
|
+
key: "$coolgrid",
|
|
33
|
+
styles,
|
|
34
|
+
css,
|
|
35
|
+
normalize: true,
|
|
36
|
+
})};
|
|
37
|
+
`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { get } from "@pyreon/ui-core"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the container max-width map using a three-layer fallback:
|
|
5
|
+
* props.width -> theme.grid.container -> theme.coolgrid.container.
|
|
6
|
+
*/
|
|
7
|
+
type GetContainerWidth = (
|
|
8
|
+
props?: Record<string, unknown> | unknown,
|
|
9
|
+
theme?: Record<string, unknown> | unknown,
|
|
10
|
+
) => ReturnType<typeof get>
|
|
11
|
+
|
|
12
|
+
export const getContainerWidth: GetContainerWidth = (props, theme) =>
|
|
13
|
+
get(props, "width") || get(theme, "grid.container") || get(theme, "coolgrid.container")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { provide, useContext } from "@pyreon/core"
|
|
2
|
+
import { PKG_NAME } from "../constants"
|
|
3
|
+
import { ContainerContext, RowContext } from "../context"
|
|
4
|
+
import type { ElementType } from "../types"
|
|
5
|
+
import useGridContext from "../useContext"
|
|
6
|
+
import { omitCtxKeys } from "../utils"
|
|
7
|
+
import Styled from "./styled"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Row component that reads inherited config from ContainerContext, merges
|
|
11
|
+
* it with its own props, and provides the resolved grid settings (columns,
|
|
12
|
+
* gap, gutter) to Col children via RowContext. Renders a flex-wrap container
|
|
13
|
+
* with negative margins to offset column gutters.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEV_PROPS: Record<string, string> =
|
|
17
|
+
process.env.NODE_ENV !== "production" ? { "data-coolgrid": "row" } : {}
|
|
18
|
+
|
|
19
|
+
const Component: ElementType<["containerWidth", "width", "rowComponent", "rowCss"]> = ({
|
|
20
|
+
children,
|
|
21
|
+
component,
|
|
22
|
+
css,
|
|
23
|
+
contentAlignX: rowAlignX,
|
|
24
|
+
...props
|
|
25
|
+
}) => {
|
|
26
|
+
const parentCtx = useContext(ContainerContext)
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
columns,
|
|
30
|
+
gap,
|
|
31
|
+
gutter,
|
|
32
|
+
rowComponent,
|
|
33
|
+
rowCss,
|
|
34
|
+
contentAlignX,
|
|
35
|
+
containerWidth,
|
|
36
|
+
size,
|
|
37
|
+
padding,
|
|
38
|
+
colCss,
|
|
39
|
+
colComponent,
|
|
40
|
+
} = useGridContext({ ...parentCtx, ...props })
|
|
41
|
+
|
|
42
|
+
const context = {
|
|
43
|
+
containerWidth,
|
|
44
|
+
size,
|
|
45
|
+
padding,
|
|
46
|
+
colCss,
|
|
47
|
+
colComponent,
|
|
48
|
+
columns,
|
|
49
|
+
gap,
|
|
50
|
+
gutter,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const finalProps = {
|
|
54
|
+
$coolgrid: {
|
|
55
|
+
contentAlignX: rowAlignX || contentAlignX,
|
|
56
|
+
columns,
|
|
57
|
+
gap,
|
|
58
|
+
gutter,
|
|
59
|
+
extraStyles: css || rowCss,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Provide row context to Col children
|
|
64
|
+
provide(RowContext, context)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Styled {...omitCtxKeys(props)} as={component || rowComponent} {...finalProps} {...DEV_PROPS}>
|
|
68
|
+
{children}
|
|
69
|
+
</Styled>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const name = `${PKG_NAME}/Row`
|
|
74
|
+
|
|
75
|
+
Component.displayName = name
|
|
76
|
+
Component.pkgName = PKG_NAME
|
|
77
|
+
Component.PYREON__COMPONENT = name
|
|
78
|
+
|
|
79
|
+
export default Component
|
package/src/Row/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { config } from "@pyreon/ui-core"
|
|
2
|
+
import type { MakeItResponsiveStyles } from "@pyreon/unistyle"
|
|
3
|
+
import { ALIGN_CONTENT_MAP_X, extendCss, makeItResponsive, value } from "@pyreon/unistyle"
|
|
4
|
+
import type { CssOutput, StyledTypes } from "../types"
|
|
5
|
+
import { isNumber } from "../utils"
|
|
6
|
+
|
|
7
|
+
const { styled, css, component } = config
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes negative horizontal margins to compensate for column gap,
|
|
11
|
+
* and vertical margins that account for gutter (inter-row spacing).
|
|
12
|
+
* This creates the classic grid pattern where column gaps cancel out
|
|
13
|
+
* at the row edges.
|
|
14
|
+
*/
|
|
15
|
+
type SpacingStyles = (
|
|
16
|
+
props: Pick<StyledTypes, "gap" | "gutter">,
|
|
17
|
+
{ rootSize }: { rootSize?: number | undefined },
|
|
18
|
+
) => CssOutput
|
|
19
|
+
|
|
20
|
+
const spacingStyles: SpacingStyles = ({ gap, gutter }, { rootSize }) => {
|
|
21
|
+
if (!isNumber(gap)) return ""
|
|
22
|
+
|
|
23
|
+
const g = gap as number
|
|
24
|
+
const getValue = (param: string | number | null | undefined) => value(param, rootSize)
|
|
25
|
+
|
|
26
|
+
const spacingX = (g / 2) * -1
|
|
27
|
+
const spacingY = isNumber(gutter) ? (gutter as number) - g / 2 : g / 2
|
|
28
|
+
|
|
29
|
+
return css`
|
|
30
|
+
margin: ${getValue(spacingY)} ${getValue(spacingX)};
|
|
31
|
+
`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Maps the contentAlignX prop to a CSS justify-content value. */
|
|
35
|
+
const contentAlign = (align?: StyledTypes["contentAlignX"]) => {
|
|
36
|
+
if (!align) return ""
|
|
37
|
+
|
|
38
|
+
return css`
|
|
39
|
+
justify-content: ${ALIGN_CONTENT_MAP_X[align]};
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Composes spacing, alignment, and extra CSS into a single responsive style block for the Row. */
|
|
44
|
+
const styles: MakeItResponsiveStyles<
|
|
45
|
+
Pick<StyledTypes, "gap" | "gutter" | "contentAlignX" | "extraStyles">
|
|
46
|
+
> = ({ theme, css: cssFn, rootSize }) => {
|
|
47
|
+
const { gap, gutter, contentAlignX, extraStyles } = theme
|
|
48
|
+
|
|
49
|
+
return cssFn`
|
|
50
|
+
${spacingStyles({ gap, gutter }, { rootSize })};
|
|
51
|
+
${contentAlign(contentAlignX)};
|
|
52
|
+
${extendCss(extraStyles)};
|
|
53
|
+
`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default styled(component)`
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
align-self: stretch;
|
|
62
|
+
flex-direction: row;
|
|
63
|
+
|
|
64
|
+
${makeItResponsive({
|
|
65
|
+
key: "$coolgrid",
|
|
66
|
+
styles,
|
|
67
|
+
css,
|
|
68
|
+
normalize: true,
|
|
69
|
+
})};
|
|
70
|
+
`
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
3
|
+
|
|
4
|
+
const mockProvide = vi.fn()
|
|
5
|
+
const mockUseContext = vi.fn()
|
|
6
|
+
|
|
7
|
+
vi.mock("@pyreon/core", async (importOriginal) => {
|
|
8
|
+
const original = await importOriginal<typeof import("@pyreon/core")>()
|
|
9
|
+
return {
|
|
10
|
+
...original,
|
|
11
|
+
provide: (...args: any[]) => {
|
|
12
|
+
mockProvide(...args)
|
|
13
|
+
},
|
|
14
|
+
useContext: (ctx: any) => {
|
|
15
|
+
if (mockUseContext.mock.results.length > 0 || mockUseContext.mock.calls.length > 0) {
|
|
16
|
+
return mockUseContext(ctx)
|
|
17
|
+
}
|
|
18
|
+
return original.useContext(ctx)
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Mock unistyle context to return empty theme
|
|
24
|
+
vi.mock("@pyreon/unistyle", async (importOriginal) => {
|
|
25
|
+
const original = await importOriginal<typeof import("@pyreon/unistyle")>()
|
|
26
|
+
return {
|
|
27
|
+
...original,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const asVNode = (v: unknown) => v as VNode
|
|
32
|
+
|
|
33
|
+
describe("Col", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks()
|
|
36
|
+
// Default: no context (empty object)
|
|
37
|
+
mockUseContext.mockReturnValue({})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("returns a VNode", async () => {
|
|
41
|
+
const Col = (await import("../Col")).default
|
|
42
|
+
const result = asVNode(Col({ children: "test" }))
|
|
43
|
+
expect(result).toBeDefined()
|
|
44
|
+
expect(result.type).toBeDefined()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("has correct displayName", async () => {
|
|
48
|
+
const Col = (await import("../Col")).default
|
|
49
|
+
expect(Col.displayName).toBe("@pyreon/coolgrid/Col")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("has correct pkgName", async () => {
|
|
53
|
+
const Col = (await import("../Col")).default
|
|
54
|
+
expect(Col.pkgName).toBe("@pyreon/coolgrid")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("has PYREON__COMPONENT static", async () => {
|
|
58
|
+
const Col = (await import("../Col")).default
|
|
59
|
+
expect(Col.PYREON__COMPONENT).toBe("@pyreon/coolgrid/Col")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("passes $coolgrid prop with grid values", async () => {
|
|
63
|
+
const Col = (await import("../Col")).default
|
|
64
|
+
const result = asVNode(Col({ size: 6, children: "test" }))
|
|
65
|
+
expect(result.props).toHaveProperty("$coolgrid")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("does not provide context (Col only reads, never provides)", async () => {
|
|
69
|
+
const Col = (await import("../Col")).default
|
|
70
|
+
Col({ children: "test" })
|
|
71
|
+
expect(mockProvide).not.toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("strips context keys from DOM props", async () => {
|
|
75
|
+
const Col = (await import("../Col")).default
|
|
76
|
+
const result = asVNode(
|
|
77
|
+
Col({
|
|
78
|
+
columns: 12,
|
|
79
|
+
gap: 16,
|
|
80
|
+
size: 6,
|
|
81
|
+
"data-testid": "my-col",
|
|
82
|
+
children: "test",
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
// context keys should be stripped from the rendered props
|
|
86
|
+
// but $coolgrid should be present
|
|
87
|
+
expect(result.props.$coolgrid).toBeDefined()
|
|
88
|
+
expect(result.props["data-testid"]).toBe("my-col")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("passes css as extraStyles when provided", async () => {
|
|
92
|
+
const Col = (await import("../Col")).default
|
|
93
|
+
const customCss = "background: green;"
|
|
94
|
+
const result = asVNode(Col({ css: customCss, children: "test" }))
|
|
95
|
+
expect((result.props.$coolgrid as Record<string, unknown>).extraStyles).toBe(customCss)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("includes columns and gap in $coolgrid", async () => {
|
|
99
|
+
const Col = (await import("../Col")).default
|
|
100
|
+
const result = asVNode(Col({ columns: 12, gap: 16, size: 6, children: "test" }))
|
|
101
|
+
const coolgrid = result.props.$coolgrid as Record<string, unknown>
|
|
102
|
+
expect(coolgrid.columns).toBe(12)
|
|
103
|
+
expect(coolgrid.gap).toBe(16)
|
|
104
|
+
expect(coolgrid.size).toBe(6)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("renders with data-coolgrid attribute in dev mode", async () => {
|
|
108
|
+
const Col = (await import("../Col")).default
|
|
109
|
+
const result = asVNode(Col({ children: "test" }))
|
|
110
|
+
expect(result.props["data-coolgrid"]).toBe("col")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("passes component prop as 'as'", async () => {
|
|
114
|
+
const Col = (await import("../Col")).default
|
|
115
|
+
const customComponent = (() => null) as any
|
|
116
|
+
const result = asVNode(Col({ component: customComponent, children: "test" }))
|
|
117
|
+
expect(result.props.as).toBe(customComponent)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("includes padding in $coolgrid", async () => {
|
|
121
|
+
const Col = (await import("../Col")).default
|
|
122
|
+
const result = asVNode(Col({ padding: 8, children: "test" }))
|
|
123
|
+
expect((result.props.$coolgrid as Record<string, unknown>).padding).toBe(8)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("renders children in VNode", async () => {
|
|
127
|
+
const Col = (await import("../Col")).default
|
|
128
|
+
const result = asVNode(Col({ children: "hello" }))
|
|
129
|
+
expect(result.children).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
})
|