diff --git a/packages/storyblok-sync/scripts/sync-components.ts b/packages/storyblok-sync/scripts/sync-components.ts index bf28c5c8..82e30d64 100755 --- a/packages/storyblok-sync/scripts/sync-components.ts +++ b/packages/storyblok-sync/scripts/sync-components.ts @@ -341,6 +341,22 @@ async function getSbContainerComponent(): Promise { }; } +const COL_SPAN_OPTIONS = [ + { name: "1", value: "1" }, + { name: "2", value: "2" }, + { name: "3", value: "3" }, + { name: "4", value: "4" }, + { name: "5", value: "5" }, + { name: "6", value: "6" }, + { name: "7", value: "7" }, + { name: "8", value: "8" }, + { name: "9", value: "9" }, + { name: "10", value: "10" }, + { name: "11", value: "11" }, + { name: "12", value: "12" }, + { name: "Full", value: "full" }, +]; + async function getSbGridComponent(): Promise { const layoutGroupUuid = await getComponentGroupUuid("Layout"); @@ -353,48 +369,89 @@ async function getSbGridComponent(): Promise { icon: "block-grid", color: "#4a5568", schema: { + // ── Tab: Content ──────────────────────────────────────────────────────── + "tab-content": { + type: "tab", + display_name: "Content", + pos: 0, + }, items: { type: "bloks", display_name: "Grid Items", required: true, restrict_components: true, component_whitelist: [], + pos: 1, + }, + // ── Tab: Layout ───────────────────────────────────────────────────────── + "tab-layout": { + type: "tab", + display_name: "Layout", + pos: 10, }, columns: { type: "option", display_name: "Columns", source: "internal", datasource_slug: "grid-columns", + pos: 11, }, columnsMd: { type: "option", display_name: "Columns (Tablet)", source: "internal", datasource_slug: "grid-columns", + pos: 12, }, columnsLg: { type: "option", display_name: "Columns (Desktop)", source: "internal", datasource_slug: "grid-columns", + pos: 13, + }, + boundingWidth: { + type: "option", + display_name: "Width", + default_value: "full", + options: [ + { name: "Full Width", value: "full" }, + { name: "Container", value: "container" }, + ], + pos: 14, + }, + isList: { + type: "boolean", + display_name: "Render as List (
    )", + default_value: "false", + pos: 15, + }, + // ── Tab: Gap & Alignment ───────────────────────────────────────────────── + "tab-gaps": { + type: "tab", + display_name: "Gap & Alignment", + pos: 20, }, gap: { type: "option", display_name: "Gap", source: "internal", datasource_slug: "spacing-options", + pos: 21, }, rowGap: { type: "option", display_name: "Row Gap", source: "internal", datasource_slug: "spacing-options", + pos: 22, }, columnGap: { type: "option", display_name: "Column Gap", source: "internal", datasource_slug: "spacing-options", + pos: 23, }, alignItems: { type: "option", @@ -405,6 +462,7 @@ async function getSbGridComponent(): Promise { { name: "End", value: "end" }, { name: "Stretch", value: "stretch" }, ], + pos: 24, }, justifyItems: { type: "option", @@ -415,6 +473,7 @@ async function getSbGridComponent(): Promise { { name: "End", value: "end" }, { name: "Stretch", value: "stretch" }, ], + pos: 25, }, justifyContent: { type: "option", @@ -427,6 +486,7 @@ async function getSbGridComponent(): Promise { { name: "Space Around", value: "space-around" }, { name: "Space Evenly", value: "space-evenly" }, ], + pos: 26, }, autoFlow: { type: "option", @@ -437,44 +497,41 @@ async function getSbGridComponent(): Promise { { name: "Row Dense", value: "row dense" }, { name: "Column Dense", value: "column dense" }, ], + pos: 27, }, - boundingWidth: { - type: "option", - display_name: "Width", - default_value: "full", - options: [ - { name: "Full Width", value: "full" }, - { name: "Container", value: "container" }, - ], + // ── Tab: Spacing ───────────────────────────────────────────────────────── + "tab-spacing": { + type: "tab", + display_name: "Spacing", + pos: 30, }, - isList: { - type: "boolean", - display_name: "Render as List", - default_value: "false", - }, - pt: { + spacingTop: { type: "option", - display_name: "Padding Top", + display_name: "Spacing Top", source: "internal", datasource_slug: "spacing-options", + pos: 31, }, - pb: { + spacingBottom: { type: "option", - display_name: "Padding Bottom", + display_name: "Spacing Bottom", source: "internal", datasource_slug: "spacing-options", + pos: 32, }, - mt: { + paddingTop: { type: "option", - display_name: "Margin Top", + display_name: "Padding Top", source: "internal", datasource_slug: "spacing-options", + pos: 33, }, - mb: { + paddingBottom: { type: "option", - display_name: "Margin Bottom", + display_name: "Padding Bottom", source: "internal", datasource_slug: "spacing-options", + pos: 34, }, }, }; @@ -492,97 +549,90 @@ async function getSbGridItemComponent(): Promise { icon: "block-square", color: "#4a5568", schema: { + // ── Tab: Content ──────────────────────────────────────────────────────── + "tab-content": { + type: "tab", + display_name: "Content", + pos: 0, + }, content: { type: "bloks", display_name: "Content", required: false, restrict_components: true, component_whitelist: [], + pos: 1, + }, + // ── Tab: Column ────────────────────────────────────────────────────────── + "tab-column": { + type: "tab", + display_name: "Column", + pos: 10, }, colSpan: { type: "option", display_name: "Column Span", - options: [ - { name: "1", value: "1" }, - { name: "2", value: "2" }, - { name: "3", value: "3" }, - { name: "4", value: "4" }, - { name: "5", value: "5" }, - { name: "6", value: "6" }, - { name: "7", value: "7" }, - { name: "8", value: "8" }, - { name: "9", value: "9" }, - { name: "10", value: "10" }, - { name: "11", value: "11" }, - { name: "12", value: "12" }, - { name: "Full", value: "full" }, - ], + options: COL_SPAN_OPTIONS, + pos: 11, }, colSpanMd: { type: "option", display_name: "Column Span (Tablet)", - options: [ - { name: "1", value: "1" }, - { name: "2", value: "2" }, - { name: "3", value: "3" }, - { name: "4", value: "4" }, - { name: "5", value: "5" }, - { name: "6", value: "6" }, - { name: "7", value: "7" }, - { name: "8", value: "8" }, - { name: "9", value: "9" }, - { name: "10", value: "10" }, - { name: "11", value: "11" }, - { name: "12", value: "12" }, - { name: "Full", value: "full" }, - ], + options: COL_SPAN_OPTIONS, + pos: 12, }, colSpanLg: { type: "option", display_name: "Column Span (Desktop)", - options: [ - { name: "1", value: "1" }, - { name: "2", value: "2" }, - { name: "3", value: "3" }, - { name: "4", value: "4" }, - { name: "5", value: "5" }, - { name: "6", value: "6" }, - { name: "7", value: "7" }, - { name: "8", value: "8" }, - { name: "9", value: "9" }, - { name: "10", value: "10" }, - { name: "11", value: "11" }, - { name: "12", value: "12" }, - { name: "Full", value: "full" }, - ], + options: COL_SPAN_OPTIONS, + pos: 13, + }, + // ── Tab: Row ───────────────────────────────────────────────────────────── + "tab-row": { + type: "tab", + display_name: "Row", + pos: 20, }, rowSpan: { type: "number", display_name: "Row Span", + pos: 21, }, rowSpanMd: { type: "number", display_name: "Row Span (Tablet)", + pos: 22, }, rowSpanLg: { type: "number", display_name: "Row Span (Desktop)", + pos: 23, + }, + // ── Tab: Position ──────────────────────────────────────────────────────── + "tab-position": { + type: "tab", + display_name: "Position", + pos: 30, }, colStart: { type: "number", display_name: "Column Start", + pos: 31, }, colEnd: { type: "number", display_name: "Column End", + pos: 32, }, rowStart: { type: "number", display_name: "Row Start", + pos: 33, }, rowEnd: { type: "number", display_name: "Row End", + pos: 34, }, }, }; diff --git a/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx b/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx index 457aac21..20695889 100644 --- a/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx +++ b/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx @@ -23,11 +23,57 @@ export interface SbGridItemProps { }; } -/** - * Storyblok Grid Item Component - * Individual grid cell with precise control over positioning and spanning - * Use inside SbGrid for editorial-style layouts with asymmetric compositions - */ +function parseColSpan( + value?: string, +): 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "full" | undefined { + if (!value) return undefined; + if (value === "full") return "full"; + const num = Number.parseInt(value, 10); + if (Number.isNaN(num) || num < 1 || num > 12) return undefined; + return num as 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; +} + +function parseRowSpan(value?: string): number | undefined { + if (!value) return undefined; + const num = Number.parseInt(value, 10); + return !Number.isNaN(num) && num >= 1 ? num : undefined; +} + +function parsePosition(value?: string): number | undefined { + if (!value) return undefined; + const num = Number.parseInt(value, 10); + return Number.isNaN(num) ? undefined : num; +} + +function colSpanToCss(value: string): string { + return value === "full" ? "1 / -1" : `span ${parseColSpan(value) ?? 1}`; +} + +function buildResponsiveStyles(uid: string, blok: SbGridItemProps["blok"]): string { + const { colSpanMd, colSpanLg, rowSpanMd, rowSpanLg } = blok; + const rules: string[] = []; + + if (colSpanMd || rowSpanMd) { + const declarations: string[] = []; + if (colSpanMd) declarations.push(`grid-column:${colSpanToCss(colSpanMd)}`); + if (rowSpanMd) declarations.push(`grid-row:span ${parseRowSpan(rowSpanMd) ?? 1}`); + rules.push( + `@media(min-width:768px){[data-sb-grid-item="${uid}"]{${declarations.join(";")}}}` + ); + } + + if (colSpanLg || rowSpanLg) { + const declarations: string[] = []; + if (colSpanLg) declarations.push(`grid-column:${colSpanToCss(colSpanLg)}`); + if (rowSpanLg) declarations.push(`grid-row:span ${parseRowSpan(rowSpanLg) ?? 1}`); + rules.push( + `@media(min-width:1024px){[data-sb-grid-item="${uid}"]{${declarations.join(";")}}}` + ); + } + + return rules.join(""); +} + export const SbGridItem = memo(function SbGridItem({ blok }: SbGridItemProps) { const { content, @@ -44,91 +90,22 @@ export const SbGridItem = memo(function SbGridItem({ blok }: SbGridItemProps) { } = blok; const editableProps = useStoryblokEditable(blok); - - // Parse column span (supports "full" value) - const parseColSpan = ( - value?: string, - ): 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "full" | undefined => { - if (!value) { - return undefined; - } - if (value === "full") { - return "full"; - } - const num = Number.parseInt(value, 10); - if (Number.isNaN(num) || num < 1 || num > 12) { - return undefined; - } - return num as 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; - }; - - // Parse row span (numeric only, no "full") - const parseRowSpan = (value?: string): number | undefined => { - if (!value) { - return undefined; - } - const num = Number.parseInt(value, 10); - return Number.isNaN(num) || num < 1 ? undefined : num; - }; - - const parsePosition = (value?: string): number | undefined => { - if (!value) { - return undefined; - } - const num = Number.parseInt(value, 10); - return Number.isNaN(num) ? undefined : num; - }; - - // Parse base span values - const baseColSpan = parseColSpan(colSpan); - const baseRowSpan = parseRowSpan(rowSpan); - - // Build responsive styles only when we have responsive values const hasPositioning = colStart || colEnd || rowStart || rowEnd; - - // Build inline styles for responsive grid positioning via style tag - const responsiveStyles = - colSpanMd || colSpanLg || rowSpanMd || rowSpanLg ? ( -