UNPKG

@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
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