@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 CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@pyreon/coolgrid",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "repository": {
5
5
  "type": "git",
6
- "url": "https://github.com/pyreon/ui-system",
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
- "bun": "./src/index.ts",
15
- "source": "./src/index.ts",
16
- "import": "./lib/index.js",
17
- "types": "./lib/index.d.ts"
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.0",
46
- "@pyreon/reactivity": "^0.11.0",
47
- "@pyreon/ui-core": "^0.11.0",
48
- "@pyreon/unistyle": "^0.11.0",
49
- "@pyreon/styler": "^0.11.0"
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.0"
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
@@ -0,0 +1,3 @@
1
+ import component from "./component"
2
+
3
+ export default component
@@ -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,3 @@
1
+ import component from "./component"
2
+
3
+ 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
@@ -0,0 +1,3 @@
1
+ import component from "./component"
2
+
3
+ export default component
@@ -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
+ })