From c831c195064f893943403fd9e35a54b64532b428 Mon Sep 17 00:00:00 2001 From: dmnktoe Date: Tue, 20 Jan 2026 23:28:28 +0100 Subject: [PATCH 1/2] chore: grid and richtext improvements --- apps/storybook/stories/Grid.stories.tsx | 182 +- packages/storyblok-richtext/src/richtext.tsx | 46 +- .../storyblok-sync/scripts/sync-components.ts | 272 +- .../scripts/sync-datasources.ts | 22 +- .../src/components/container/SbContainer.tsx | 130 +- .../src/components/grid-item/SbGridItem.tsx | 162 +- .../src/components/grid/SbGrid.tsx | 30 +- .../src/components/headline/SbHeadline.tsx | 17 +- .../src/components/image/SbImage.tsx | 241 +- .../media-wrapper/SbMediaWrapper.tsx | 44 - .../src/components/media-wrapper/index.ts | 2 - .../src/components/paragraph/SbParagraph.tsx | 18 +- .../src/components/section/SbSection.tsx | 102 +- .../src/components/slideshow/SbSlideshow.tsx | 164 +- .../src/components/video/SbVideo.tsx | 198 +- packages/storyblok-ui/src/index.ts | 4 - .../storyblok-ui/src/lib/spacing-utils.ts | 20 +- packages/ui/panda.config.ts | 40 +- packages/ui/src/components/grid/grid-item.tsx | 32 +- packages/ui/src/components/list/list-item.tsx | 10 +- .../ui/src/components/list/ordered-list.tsx | 11 +- .../ui/src/components/list/unordered-list.tsx | 9 +- .../ui/src/components/paragraph/paragraph.tsx | 14 +- packages/ui/styles.css | 8872 +++++++++++++---- 24 files changed, 8388 insertions(+), 2254 deletions(-) delete mode 100644 packages/storyblok-ui/src/components/media-wrapper/SbMediaWrapper.tsx delete mode 100644 packages/storyblok-ui/src/components/media-wrapper/index.ts diff --git a/apps/storybook/stories/Grid.stories.tsx b/apps/storybook/stories/Grid.stories.tsx index 6d1cff84..32ccee82 100644 --- a/apps/storybook/stories/Grid.stories.tsx +++ b/apps/storybook/stories/Grid.stories.tsx @@ -330,6 +330,7 @@ export const BasicWithBoxes: Story = { /** * Magazine-style 12-column grid - Editorial Artsy Portfolio + * Fully responsive: stacked on mobile, 2-column on tablet, 12-column on desktop */ export const MagazineLayout: Story = { args: { @@ -338,9 +339,20 @@ export const MagazineLayout: Story = { gap: 0, }, render: (args) => ( - + {/* Hero Image - Breathe */} - + Outlet Store Hero {/* Title - Generous spacing */} - - + + ⋆.˚ ᑣ𐭩 Creative Direction @@ -371,8 +396,12 @@ export const MagazineLayout: Story = { {/* Large Portrait Left */} - - + + Portrait {/* Small Square Top Right */} - - + + Outlet Store Detail {/* Text Block Float */} - - + + πŸŽ€ ΰ­§κ”›κ—ƒΛ– Embracing the space between elements creates rhythm and allows each piece to breathe ο½₯οΎŸβ‹† @@ -403,8 +445,17 @@ export const MagazineLayout: Story = { {/* Wide Image Center */} - - + + Outlet Store Wide {/* Quote - Minimal */} - - + + {/* Two Images Offset */} - + Left - - + + Right - {/* Small Detail Right */} - - - Small detail + {/* Small Detail Right - Full Bleed */} + + + + Small detail + {/* Closing Image Full Bleed */} - - + + Closing {/* Footer Details */} - - + + {/* Wide Image - Bottom */} - - Wide Shot + + + Wide Shot + ), diff --git a/packages/storyblok-richtext/src/richtext.tsx b/packages/storyblok-richtext/src/richtext.tsx index 71a7027b..62a33a1d 100644 --- a/packages/storyblok-richtext/src/richtext.tsx +++ b/packages/storyblok-richtext/src/richtext.tsx @@ -114,8 +114,9 @@ function MarkCode(children: ReactNode) { */ function NodeHeading(children: ReactNode, props: any) { const level = props.level as 1 | 2 | 3; + const textAlign = props.textAlign; return ( - + {children} ); @@ -123,17 +124,26 @@ function NodeHeading(children: ReactNode, props: any) { /** * Custom resolver for paragraphs - * Accepts maxWidth from context for dynamic prose width - * Filters out empty paragraphs to avoid unnecessary spacing + * Accepts maxWidth from context and textAlign from node attributes + * Spacing is controlled via CSS on the parent container */ function NodeParagraph( children: ReactNode, - _props: any, - context?: { maxWidth?: string | boolean }, + props: any, + context?: { + maxWidth?: string | boolean; + }, ) { const maxWidth = context?.maxWidth ?? true; + const textAlign = props.textAlign as + | "left" + | "center" + | "right" + | "justify" + | undefined; + return ( - + {children} ); @@ -164,7 +174,11 @@ function NodeImage(_children: ReactNode, props: any) { * Custom resolver for unordered lists */ function NodeUL(children: ReactNode) { - return {children}; + return ( + + {children} + + ); } /** @@ -252,7 +266,9 @@ function DefaultBlokResolver(_name: string, _props: any) { */ export function renderStoryblokRichText( data: ISbRichtext, - options?: { maxWidth?: string | boolean }, + options?: { + maxWidth?: string | boolean; + }, ) { if (!data) { return null; @@ -308,5 +324,17 @@ export function StoryblokRichText({ const content = renderStoryblokRichText(data, { maxWidth }); - return {content}; + return ( + p + p": { + mt: "4", + }, + }} + {...props} + > + {content} + + ); } diff --git a/packages/storyblok-sync/scripts/sync-components.ts b/packages/storyblok-sync/scripts/sync-components.ts index bf28c5c8..50a47efb 100755 --- a/packages/storyblok-sync/scripts/sync-components.ts +++ b/packages/storyblok-sync/scripts/sync-components.ts @@ -177,20 +177,34 @@ async function getSbSectionComponent(): Promise { datasource_slug: "spacing-options", pos: 7, }, + marginLeft: { + type: "option", + display_name: "Margin Left", + source: "internal", + datasource_slug: "spacing-options", + pos: 8, + }, + marginRight: { + type: "option", + display_name: "Margin Right", + source: "internal", + datasource_slug: "spacing-options", + pos: 9, + }, paddingTopMd: { type: "option", display_name: "Padding Top (Tablet)", description: "Override padding for tablet screens", source: "internal", datasource_slug: "spacing-options", - pos: 8, + pos: 10, }, paddingBottomMd: { type: "option", display_name: "Padding Bottom (Tablet)", source: "internal", datasource_slug: "spacing-options", - pos: 9, + pos: 11, }, paddingLeftMd: { type: "option", @@ -228,6 +242,54 @@ async function getSbSectionComponent(): Promise { source: "internal", datasource_slug: "spacing-options", }, + marginTopMd: { + type: "option", + display_name: "Margin Top (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginBottomMd: { + type: "option", + display_name: "Margin Bottom (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginTopLg: { + type: "option", + display_name: "Margin Top (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginBottomLg: { + type: "option", + display_name: "Margin Bottom (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginLeftMd: { + type: "option", + display_name: "Margin Left (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginRightMd: { + type: "option", + display_name: "Margin Right (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginLeftLg: { + type: "option", + display_name: "Margin Left (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + marginRightLg: { + type: "option", + display_name: "Margin Right (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, }, }; } @@ -307,6 +369,18 @@ async function getSbContainerComponent(): Promise { source: "internal", datasource_slug: "spacing-options", }, + ml: { + type: "option", + display_name: "Margin Left", + source: "internal", + datasource_slug: "spacing-options", + }, + mr: { + type: "option", + display_name: "Margin Right", + source: "internal", + datasource_slug: "spacing-options", + }, bgColor: { type: "option", display_name: "Background Color", @@ -337,6 +411,90 @@ async function getSbContainerComponent(): Promise { source: "internal", datasource_slug: "spacing-options", }, + ptMd: { + type: "option", + display_name: "Padding Top (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + pbMd: { + type: "option", + display_name: "Padding Bottom (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + ptLg: { + type: "option", + display_name: "Padding Top (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + pbLg: { + type: "option", + display_name: "Padding Bottom (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + mtMd: { + type: "option", + display_name: "Margin Top (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + mbMd: { + type: "option", + display_name: "Margin Bottom (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + mtLg: { + type: "option", + display_name: "Margin Top (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + mbLg: { + type: "option", + display_name: "Margin Bottom (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + myMd: { + type: "option", + display_name: "Margin Vertical (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + myLg: { + type: "option", + display_name: "Margin Vertical (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + mlMd: { + type: "option", + display_name: "Margin Left (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + mrMd: { + type: "option", + display_name: "Margin Right (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + mlLg: { + type: "option", + display_name: "Margin Left (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + mrLg: { + type: "option", + display_name: "Margin Right (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, }, }; } @@ -452,6 +610,24 @@ async function getSbGridComponent(): Promise { display_name: "Render as List", default_value: "false", }, + bgColor: { + type: "option", + display_name: "Background Color", + source: "internal", + datasource_slug: "color-options", + }, + px: { + type: "option", + display_name: "Padding Horizontal", + source: "internal", + datasource_slug: "spacing-options", + }, + py: { + type: "option", + display_name: "Padding Vertical", + source: "internal", + datasource_slug: "spacing-options", + }, pt: { type: "option", display_name: "Padding Top", @@ -464,6 +640,30 @@ async function getSbGridComponent(): Promise { source: "internal", datasource_slug: "spacing-options", }, + pxMd: { + type: "option", + display_name: "Padding Horizontal (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + pyMd: { + type: "option", + display_name: "Padding Vertical (Tablet)", + source: "internal", + datasource_slug: "spacing-options", + }, + pxLg: { + type: "option", + display_name: "Padding Horizontal (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, + pyLg: { + type: "option", + display_name: "Padding Vertical (Desktop)", + source: "internal", + datasource_slug: "spacing-options", + }, mt: { type: "option", display_name: "Margin Top", @@ -476,6 +676,18 @@ async function getSbGridComponent(): Promise { source: "internal", datasource_slug: "spacing-options", }, + ml: { + type: "option", + display_name: "Margin Left", + source: "internal", + datasource_slug: "spacing-options", + }, + mr: { + type: "option", + display_name: "Margin Right", + source: "internal", + datasource_slug: "spacing-options", + }, }, }; } @@ -572,10 +784,26 @@ async function getSbGridItemComponent(): Promise { type: "number", display_name: "Column Start", }, + colStartMd: { + type: "number", + display_name: "Column Start (Tablet)", + }, + colStartLg: { + type: "number", + display_name: "Column Start (Desktop)", + }, colEnd: { type: "number", display_name: "Column End", }, + colEndMd: { + type: "number", + display_name: "Column End (Tablet)", + }, + colEndLg: { + type: "number", + display_name: "Column End (Desktop)", + }, rowStart: { type: "number", display_name: "Row Start", @@ -631,6 +859,31 @@ async function getSbHeadlineComponent(): Promise { source: "internal", datasource_slug: "color-options", }, + fontSize: { + type: "text", + display_name: "Font Size", + description: "Custom font size (e.g., '2rem', '24px')", + }, + lineHeight: { + type: "text", + display_name: "Line Height", + description: "Custom line height (e.g., '1.2', '1.5')", + }, + fontWeight: { + type: "text", + display_name: "Font Weight", + description: "Font weight (e.g., '400', '700', 'bold')", + }, + opacity: { + type: "text", + display_name: "Opacity", + description: "Opacity value (0-1, e.g., '0.5')", + }, + letterSpacing: { + type: "text", + display_name: "Letter Spacing", + description: "Letter spacing (e.g., '0.1em', '2px')", + }, marginTop: { type: "option", display_name: "Margin Top", @@ -698,6 +951,21 @@ async function getSbParagraphComponent(): Promise { { name: "Right", value: "right" }, ], }, + lineHeight: { + type: "text", + display_name: "Line Height", + description: "Custom line height (e.g., '1.5', '2')", + }, + opacity: { + type: "text", + display_name: "Opacity", + description: "Opacity value (0-1, e.g., '0.7')", + }, + letterSpacing: { + type: "text", + display_name: "Letter Spacing", + description: "Letter spacing (e.g., '0.15em', '2px')", + }, color: { type: "option", display_name: "Text Color", diff --git a/packages/storyblok-sync/scripts/sync-datasources.ts b/packages/storyblok-sync/scripts/sync-datasources.ts index 73422285..837e49d3 100755 --- a/packages/storyblok-sync/scripts/sync-datasources.ts +++ b/packages/storyblok-sync/scripts/sync-datasources.ts @@ -140,25 +140,35 @@ async function createOrUpdateDatasource( /** * Generate spacing options datasource dynamically from tokens + * Includes both positive and negative values for overlapping layouts */ function generateSpacingDatasource(): { datasource: Datasource; entries: DatasourceEntry[]; } { - // Dynamically generate from spacing tokens - const spacingEntries = Object.entries(spacing) + // Generate positive values from spacing tokens + const positiveEntries = Object.entries(spacing) .sort(([a], [b]) => Number(a) - Number(b)) .map(([key, value]) => ({ name: `${key} (${value})`, value: key, })); + // Generate negative values (excluding 0) + const negativeEntries = Object.entries(spacing) + .filter(([key]) => key !== "0") + .sort(([a], [b]) => Number(b) - Number(a)) // Reverse sort for negatives + .map(([key, value]) => ({ + name: `Negative ${key} (-${value})`, + value: `neg${key}`, + })); + return { datasource: { name: "Spacing Options", slug: "spacing-options", }, - entries: spacingEntries, + entries: [...positiveEntries, ...negativeEntries], }; } @@ -278,6 +288,12 @@ function generateGridColumnsDatasource(): { { name: "4 Columns", value: "4" }, { name: "5 Columns", value: "5" }, { name: "6 Columns", value: "6" }, + { name: "7 Columns", value: "7" }, + { name: "8 Columns", value: "8" }, + { name: "9 Columns", value: "9" }, + { name: "10 Columns", value: "10" }, + { name: "11 Columns", value: "11" }, + { name: "12 Columns", value: "12" }, { name: "Auto Fit", value: "auto-fit" }, ]; diff --git a/packages/storyblok-ui/src/components/container/SbContainer.tsx b/packages/storyblok-ui/src/components/container/SbContainer.tsx index c98a01e2..a7cf9aaa 100644 --- a/packages/storyblok-ui/src/components/container/SbContainer.tsx +++ b/packages/storyblok-ui/src/components/container/SbContainer.tsx @@ -20,6 +20,8 @@ export interface SbContainerProps { mt?: string; mb?: string; my?: string; + ml?: string; + mr?: string; center?: boolean | string; bgColor?: string; // Responsive overrides (optional) @@ -27,6 +29,20 @@ export interface SbContainerProps { pyMd?: string; pxLg?: string; pyLg?: string; + ptMd?: string; + pbMd?: string; + ptLg?: string; + pbLg?: string; + mtMd?: string; + mbMd?: string; + mtLg?: string; + mbLg?: string; + myMd?: string; + myLg?: string; + mlMd?: string; + mrMd?: string; + mlLg?: string; + mrLg?: string; }; } @@ -48,11 +64,27 @@ export const SbContainer = memo(function SbContainer({ mt, mb, my, + ml, + mr, bgColor, pxMd, pyMd, pxLg, pyLg, + ptMd, + pbMd, + ptLg, + pbLg, + mtMd, + mbMd, + mtLg, + mbLg, + myMd, + myLg, + mlMd, + mrMd, + mlLg, + mrLg, } = blok; const editableProps = useStoryblokEditable(blok); @@ -64,25 +96,103 @@ export const SbContainer = memo(function SbContainer({ // Build responsive spacing objects const pxValue = mapSpacingToToken(px); const pyValue = mapSpacingToToken(py); + const ptValue = mapSpacingToToken(pt); + const pbValue = mapSpacingToToken(pb); + const mtValue = mapSpacingToToken(mt); + const mbValue = mapSpacingToToken(mb); + const myValue = mapSpacingToToken(my); + const mlValue = mapSpacingToToken(ml); + const mrValue = mapSpacingToToken(mr); + + const filterUndefined = >( + obj: T, + ): Record => { + return Object.fromEntries( + Object.entries(obj).filter(([, value]) => value !== undefined), + ) as Record; + }; const responsivePx = pxMd || pxLg - ? { + ? filterUndefined({ base: pxValue, md: mapSpacingToToken(pxMd) || pxValue, lg: mapSpacingToToken(pxLg) || mapSpacingToToken(pxMd) || pxValue, - } + }) : pxValue; const responsivePy = pyMd || pyLg - ? { + ? filterUndefined({ base: pyValue, md: mapSpacingToToken(pyMd) || pyValue, lg: mapSpacingToToken(pyLg) || mapSpacingToToken(pyMd) || pyValue, - } + }) : pyValue; + const responsivePt = + ptMd || ptLg + ? filterUndefined({ + base: ptValue, + md: mapSpacingToToken(ptMd) || ptValue, + lg: mapSpacingToToken(ptLg) || mapSpacingToToken(ptMd) || ptValue, + }) + : ptValue; + + const responsivePb = + pbMd || pbLg + ? filterUndefined({ + base: pbValue, + md: mapSpacingToToken(pbMd) || pbValue, + lg: mapSpacingToToken(pbLg) || mapSpacingToToken(pbMd) || pbValue, + }) + : pbValue; + + const responsiveMt = + mtMd || mtLg + ? filterUndefined({ + base: mtValue, + md: mapSpacingToToken(mtMd) || mtValue, + lg: mapSpacingToToken(mtLg) || mapSpacingToToken(mtMd) || mtValue, + }) + : mtValue; + + const responsiveMb = + mbMd || mbLg + ? filterUndefined({ + base: mbValue, + md: mapSpacingToToken(mbMd) || mbValue, + lg: mapSpacingToToken(mbLg) || mapSpacingToToken(mbMd) || mbValue, + }) + : mbValue; + + const responsiveMy = + myMd || myLg + ? filterUndefined({ + base: myValue, + md: mapSpacingToToken(myMd) || myValue, + lg: mapSpacingToToken(myLg) || mapSpacingToToken(myMd) || myValue, + }) + : myValue; + + const responsiveMl = + mlMd || mlLg + ? filterUndefined({ + base: mlValue, + md: mapSpacingToToken(mlMd) || mlValue, + lg: mapSpacingToToken(mlLg) || mapSpacingToToken(mlMd) || mlValue, + }) + : mlValue; + + const responsiveMr = + mrMd || mrLg + ? filterUndefined({ + base: mrValue, + md: mapSpacingToToken(mrMd) || mrValue, + lg: mapSpacingToToken(mrLg) || mapSpacingToToken(mrMd) || mrValue, + }) + : mrValue; + return ( diff --git a/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx b/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx index 457aac21..a57b3253 100644 --- a/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx +++ b/packages/storyblok-ui/src/components/grid-item/SbGridItem.tsx @@ -2,6 +2,7 @@ import { type BlokItem, DynamicRender } from "@httpjpg/storyblok-utils"; import { GridItem } from "@httpjpg/ui"; +import { css } from "@httpjpg/ui/css"; import type { SbBlokData } from "@storyblok/react/rsc"; import { memo } from "react"; import { useStoryblokEditable } from "../../lib/use-storyblok-editable"; @@ -17,7 +18,11 @@ export interface SbGridItemProps { rowSpanMd?: string; rowSpanLg?: string; colStart?: string; + colStartMd?: string; + colStartLg?: string; colEnd?: string; + colEndMd?: string; + colEndLg?: string; rowStart?: string; rowEnd?: string; }; @@ -38,7 +43,11 @@ export const SbGridItem = memo(function SbGridItem({ blok }: SbGridItemProps) { rowSpanMd, rowSpanLg, colStart, + colStartMd, + colStartLg, colEnd, + colEndMd, + colEndLg, rowStart, rowEnd, } = blok; @@ -83,62 +92,105 @@ export const SbGridItem = memo(function SbGridItem({ blok }: SbGridItemProps) { 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 ? ( -