@vitus-labs/coolgrid
Version:
Ultra flexible and extensible grid system inspired by Bootstrap grid, based on styled-components
437 lines (416 loc) • 11.8 kB
JavaScript
import { ALIGN_CONTENT_MAP_X, Provider, context, extendCss, makeItResponsive, value } from "@vitus-labs/unistyle";
import { createContext, useCallback, useContext, useMemo, useState } from "react";
import { config, get, omit, pick, useStableValue } from "@vitus-labs/core";
import { jsx } from "react/jsx-runtime";
//#region src/constants.ts
const PKG_NAME = "@vitus-labs/coolgrid";
/**
* Grid configuration keys that are passed through React context
* from Container to Row and from Row to Col components.
*/
const CONTEXT_KEYS = [
"columns",
"size",
"gap",
"padding",
"gutter",
"colCss",
"colComponent",
"rowCss",
"rowComponent",
"contentAlignX"
];
//#endregion
//#region src/context/ContainerContext.ts
/**
* React context for container-level grid configuration.
* Provided by the Container component and consumed by Row children
* to inherit columns, gap, gutter, and other grid settings.
*/
var ContainerContext_default = createContext({});
//#endregion
//#region src/context/RowContext.ts
/**
* React context for row-level grid configuration.
* Provided by the Row component and consumed by Col children
* to inherit columns, gap, gutter, and sizing for width calculations.
*/
var RowContext_default = createContext({});
//#endregion
//#region src/useContext.tsx
const pickThemeProps = (props, keywords) => pick(props, keywords);
const getGridContext = (props = {}, theme = {}) => ({
columns: get(props, "columns") || get(theme, "grid.columns") || get(theme, "coolgrid.columns"),
containerWidth: get(props, "width") || get(theme, "grid.container") || get(theme, "coolgrid.container")
});
const useGridContext = (props) => {
const { theme } = useContext(context);
const stableCtxProps = useStableValue(pickThemeProps(props, CONTEXT_KEYS));
return useMemo(() => {
return {
...getGridContext(stableCtxProps, theme),
...stableCtxProps
};
}, [stableCtxProps, theme]);
};
//#endregion
//#region src/utils.ts
/** Checks whether a value is a finite number. */
const isNumber = (value) => Number.isFinite(value);
/** Checks whether a value is a finite number greater than zero. */
const hasValue = (value) => isNumber(value) && value > 0;
/**
* Determines if a column should be visible. A column is visible when its
* size is undefined (auto) or a non-zero number. Size 0 hides the column.
*/
const isVisible = (value) => isNumber(value) && value !== 0 || value === void 0;
const omitCtxKeys = (props) => omit(props, CONTEXT_KEYS);
//#endregion
//#region src/Col/styled.ts
const { styled: styled$2, css: css$2, component: component$2 } = config;
/** Returns true when both size and columns are valid, enabling explicit width calculation. */
const hasWidth = (size, columns) => hasValue(size) && hasValue(columns);
/**
* Calculates column width as a percentage of total columns, subtracting
* the gap when present. On web uses `calc(%)`, on native uses absolute pixels.
*/
const widthStyles = ({ size, columns, gap, RNparentWidth }, { rootSize }) => {
if (!hasWidth(size, columns)) return "";
const s = size;
const c = columns;
const g = gap;
if (!RNparentWidth) return "";
const width = RNparentWidth / c * s;
return css$2`
flex-grow: 0;
flex-shrink: 0;
${css$2`
width: ${value(hasValue(gap) ? Math.max(0, width - g) : width, rootSize)};
flex-basis: auto;
`}
`;
};
/** Applies half of the given value as either margin or padding (used for gap and padding distribution). */
const spacingStyles$1 = (type, param, rootSize) => {
if (!isNumber(param)) return "";
return css$2`
${`${type}: ${value(param / 2, rootSize)}`};
`;
};
/**
* Main responsive style block for Col. When the column is visible, applies
* width, padding, margin, and extra CSS. When hidden (size === 0), moves
* the element off-screen with fixed positioning.
*/
const styles$2 = ({ theme, css, rootSize }) => {
const { size, columns, gap, padding, extraStyles, RNparentWidth } = theme;
if (isVisible(size)) return css`
${""}
position: relative;
${widthStyles({
size,
columns,
gap,
RNparentWidth
}, { rootSize })};
${spacingStyles$1("padding", padding, rootSize)};
${spacingStyles$1("margin", gap, rootSize)};
${extendCss(extraStyles)};
`;
return css`
left: -9999px;
position: ${"absolute"};
margin: 0;
padding: 0;
`;
};
var styled_default$2 = styled$2(component$2)`
${false}
position: relative;
display: flex;
flex-basis: 0;
flex-grow: 1;
flex-direction: column;
${makeItResponsive({
key: "$coolgrid",
styles: styles$2,
css: css$2,
normalize: true
})};
`;
//#endregion
//#region src/Col/component.native.tsx
/**
* Native Col component that reads RNparentWidth from RowContext
* and passes it into the styled component so column widths can be
* computed as absolute pixels (CSS calc() is unavailable on RN).
*/
const Component$2 = ({ children, component, css, ...props }) => {
const parentCtx = useContext(RowContext_default);
const { colCss, colComponent, columns, gap, size, padding } = useGridContext({
...parentCtx,
...props
});
const RNparentWidth = parentCtx.RNparentWidth ?? 0;
const finalProps = useMemo(() => ({ $coolgrid: {
columns,
gap,
size,
padding,
RNparentWidth,
extraStyles: css ?? colCss
} }), [
columns,
gap,
size,
padding,
RNparentWidth,
css,
colCss
]);
return /* @__PURE__ */ jsx(styled_default$2, {
...omitCtxKeys(props),
as: component ?? colComponent,
...finalProps,
children
});
};
const name$2 = `${PKG_NAME}/Col`;
Component$2.displayName = name$2;
Component$2.pkgName = PKG_NAME;
Component$2.VITUS_LABS__COMPONENT = name$2;
//#endregion
//#region src/Col/index.ts
var Col_default = Component$2;
//#endregion
//#region src/Container/styled.ts
const { styled: styled$1, css: css$1, component: component$1 } = config;
/** Responsive styles that apply the container's max-width and any extra CSS at each breakpoint. */
const styles$1 = ({ theme: t, css, rootSize }) => {
const w = t.width != null && typeof t.width !== "object" ? t.width : null;
return css`
${w != null ? `max-width: ${value(w, rootSize)};` : ""};
${extendCss(t.extraStyles)};
`;
};
/** Styled Container element. Centered via auto margins with responsive max-width. */
var styled_default$1 = styled$1(component$1)`
display: flex;
flex-direction: column;
${false}
${makeItResponsive({
key: "$coolgrid",
styles: styles$1,
css: css$1,
normalize: true
})};
`;
//#endregion
//#region src/Container/component.tsx
/**
* Container component that establishes the outermost grid boundary.
* Resolves grid config from the theme, provides it to descendant Row/Col
* components via ContainerContext, and renders a styled wrapper with
* responsive max-width.
*/
const DEV_PROPS = process.env.NODE_ENV !== "production" ? { "data-coolgrid": "container" } : {};
const Component$1 = ({ children, component, css, width, ...props }) => {
const { containerWidth, columns, size, gap, padding, gutter, colCss, colComponent, rowCss, rowComponent, contentAlignX } = useGridContext(props);
const context = useMemo(() => ({
columns,
size,
gap,
padding,
gutter,
colCss,
colComponent,
rowCss,
rowComponent,
contentAlignX
}), [
columns,
size,
gap,
padding,
gutter,
colCss,
colComponent,
rowCss,
rowComponent,
contentAlignX
]);
const finalWidth = useMemo(() => {
if (!width) return containerWidth;
return typeof width === "function" ? width(containerWidth) : width;
}, [width, containerWidth]);
const finalProps = useMemo(() => ({ $coolgrid: {
width: finalWidth,
extraStyles: css
} }), [finalWidth, css]);
return /* @__PURE__ */ jsx(styled_default$1, {
...omitCtxKeys(props),
as: component,
...finalProps,
...DEV_PROPS,
children: /* @__PURE__ */ jsx(ContainerContext_default.Provider, {
value: context,
children
})
});
};
const name$1 = `${PKG_NAME}/Container`;
Component$1.displayName = name$1;
Component$1.pkgName = PKG_NAME;
Component$1.VITUS_LABS__COMPONENT = name$1;
//#endregion
//#region src/Container/index.ts
var Container_default = Component$1;
//#endregion
//#region src/Row/styled.ts
const { styled, css, component } = config;
const spacingStyles = ({ gap, gutter }, { rootSize }) => {
if (!isNumber(gap)) return "";
const g = gap;
const getValue = (param) => value(param, rootSize);
const spacingX = g / 2 * -1;
const spacingY = isNumber(gutter) ? gutter - g / 2 : g / 2;
return css`
margin-top: ${getValue(spacingY)};
margin-bottom: ${getValue(spacingY)};
margin-left: ${getValue(spacingX)};
margin-right: ${getValue(spacingX)};
`;
};
/** Maps the contentAlignX prop to a CSS justify-content value. */
const contentAlign = (align) => {
if (!align) return "";
return css`
justify-content: ${ALIGN_CONTENT_MAP_X[align]};
`;
};
/** Composes spacing, alignment, and extra CSS into a single responsive style block for the Row. */
const styles = ({ theme, css, rootSize }) => {
const { gap, gutter, contentAlignX, extraStyles } = theme;
return css`
${spacingStyles({
gap,
gutter
}, { rootSize })};
${contentAlign(contentAlignX)};
${extendCss(extraStyles)};
`;
};
var styled_default = styled(component)`
${false};
display: flex;
flex-wrap: wrap;
align-self: stretch;
flex-direction: row;
${makeItResponsive({
key: "$coolgrid",
styles,
css,
normalize: true
})};
`;
//#endregion
//#region src/Row/component.native.tsx
/**
* Native Row component that measures its own width via onLayout and passes
* it as RNparentWidth through RowContext so Col children can compute
* absolute pixel widths (CSS calc() is unavailable on React Native).
*/
const Component = ({ children, component, css, contentAlignX: rowAlignX, ...props }) => {
const parentCtx = useContext(ContainerContext_default);
const [parentWidth, setParentWidth] = useState(0);
const { columns, gap, gutter, rowComponent, rowCss, contentAlignX, containerWidth, size, padding, colCss, colComponent } = useGridContext({
...parentCtx,
...props
});
const context = useMemo(() => ({
containerWidth,
size,
padding,
colCss,
colComponent,
columns,
gap,
gutter,
RNparentWidth: parentWidth
}), [
containerWidth,
size,
padding,
colCss,
colComponent,
columns,
gap,
gutter,
parentWidth
]);
const finalProps = useMemo(() => ({ $coolgrid: {
contentAlignX: rowAlignX || contentAlignX,
columns,
gap,
gutter,
extraStyles: css || rowCss
} }), [
rowAlignX,
contentAlignX,
columns,
gap,
gutter,
css,
rowCss
]);
const onLayout = useCallback((e) => {
const newWidth = e?.nativeEvent?.layout?.width;
if (newWidth != null) setParentWidth((prev) => prev === newWidth ? prev : newWidth);
}, []);
return /* @__PURE__ */ jsx(styled_default, {
...omitCtxKeys(props),
as: component || rowComponent,
...finalProps,
onLayout,
children: /* @__PURE__ */ jsx(RowContext_default.Provider, {
value: context,
children
})
});
};
const name = `${PKG_NAME}/Row`;
Component.displayName = name;
Component.pkgName = PKG_NAME;
Component.VITUS_LABS__COMPONENT = name;
//#endregion
//#region src/Row/index.ts
var Row_default = Component;
//#endregion
//#region src/theme.ts
/**
* Default Bootstrap-like grid configuration. Provides 5 breakpoints (xs-xl),
* a 12-column grid, and responsive container max-widths matching Bootstrap 4.
*/
var theme_default = {
rootSize: 16,
breakpoints: {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200
},
grid: {
columns: 12,
container: {
xs: "100%",
sm: 540,
md: 720,
lg: 960,
xl: 1140
}
}
};
//#endregion
export { Col_default as Col, Container_default as Container, Provider, Row_default as Row, theme_default as theme };
//# sourceMappingURL=vitus-labs-coolgrid.native.js.map