@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.
@@ -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
+ })
@@ -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>({})
@@ -0,0 +1,4 @@
1
+ import ContainerContext from "./ContainerContext"
2
+ import RowContext from "./RowContext"
3
+
4
+ export { ContainerContext, RowContext }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Provider } from "@pyreon/unistyle"
2
+ import Col from "./Col"
3
+ import Container from "./Container"
4
+ import Row from "./Row"
5
+ import theme from "./theme"
6
+
7
+ export { Col, Container, Provider, Row, theme }
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)