diff --git a/src/components/puck/blocks/grid.tsx b/src/components/puck/blocks/grid.tsx index 4aae5d3..cea6faa 100644 --- a/src/components/puck/blocks/grid.tsx +++ b/src/components/puck/blocks/grid.tsx @@ -1,8 +1,13 @@ -import type { ComponentConfig, Slot } from "@puckeditor/core"; +"use client"; + +import { createUsePuck, type ComponentConfig, Slot } from "@puckeditor/core"; import { defineProps, responsive, field } from "@/lib/puck/define-props"; import type { ResponsiveValue } from "@/lib/puck/responsive"; import { columnCount, gap, gridRows, type ColumnCount, type Spacing, type GridRows } from "@/lib/puck/tokens"; -import { getGridClassName } from "@/lib/puck/layout"; +import { cn } from "@/lib/utils"; +import { getGridCapacity, getGridClassName } from "@/lib/puck/layout"; + +const usePuck = createUsePuck(); type GridProps = { content: Slot; @@ -22,7 +27,11 @@ export const Grid: ComponentConfig = { label: "Grid", inline: true, ...props, - render: ({ content: Content, columns, rows: r, gap, puck }) => { + render: ({ content: Content, columns, rows: r, gap, puck, id }) => { + const viewportWidth = usePuck((state) => state.appState.ui.viewports.current.width); + const capacity = getGridCapacity(columns, r, viewportWidth); + const overflowClassName = `grid-overflow-${id}`; + if (!Content) { return (
= { ); } + const className = cn(getGridClassName({ columns, rows: r, gap }), overflowClassName); + const overflowStyle = capacity !== null + ? `.${overflowClassName} > [data-puck-component]:nth-child(n + ${capacity + 1}) { display: none; }` + : null; + return ( - +
+ {overflowStyle && } + + {/* Hidden items are visually clamped via injected CSS when capacity is set. */} +
); }, }; diff --git a/src/lib/puck/layout.test.ts b/src/lib/puck/layout.test.ts index 8055d3c..d574a36 100644 --- a/src/lib/puck/layout.test.ts +++ b/src/lib/puck/layout.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { getContainerSlotClassName, getContainerSurfaceClassName, + getGridCapacity, getGridClassName, getMaxCols, } from "./layout"; @@ -45,4 +46,22 @@ describe("layout helpers", () => { it("returns the largest configured breakpoint column count", () => { expect(getMaxCols({ base: "1", md: "3", lg: "2" })).toBe(3); }); + + it("resolves grid capacity from the active responsive values", () => { + expect( + getGridCapacity( + { base: "1", md: "3" }, + { base: "auto", md: "2" }, + 500, + ), + ).toBeNull(); + + expect( + getGridCapacity( + { base: "1", md: "3" }, + { base: "auto", md: "2" }, + 1280, + ), + ).toBe(6); + }); }); diff --git a/src/lib/puck/layout.ts b/src/lib/puck/layout.ts index 462a2ef..37fd840 100644 --- a/src/lib/puck/layout.ts +++ b/src/lib/puck/layout.ts @@ -95,3 +95,46 @@ export function getGridClassName({ export function getMaxCols(columns: ResponsiveValue): number { return Math.max(...Object.values(columns).map(Number)); } + +const responsiveViewportWidths = { + md: 768, + lg: 1024, +} as const; + +function resolveResponsiveValue( + value: ResponsiveValue, + viewportWidth: number | "100%", +): T { + const orderedBreakpoints = viewportWidth === "100%" + ? ["lg", "md", "base"] as const + : viewportWidth >= responsiveViewportWidths.lg + ? ["lg", "md", "base"] as const + : viewportWidth >= responsiveViewportWidths.md + ? ["md", "base"] as const + : ["base"] as const; + + for (const breakpoint of orderedBreakpoints) { + const breakpointValue = value[breakpoint]; + + if (breakpointValue !== undefined) { + return breakpointValue; + } + } + + return value.base; +} + +export function getGridCapacity( + columns: ResponsiveValue, + rows: ResponsiveValue, + viewportWidth: number | "100%", +): number | null { + const resolvedRows = resolveResponsiveValue(rows, viewportWidth); + + if (resolvedRows === "auto") { + return null; + } + + const resolvedColumns = Number(resolveResponsiveValue(columns, viewportWidth)); + return resolvedColumns * Number(resolvedRows); +}