diff --git a/.changeset/polite-llamas-cry.md b/.changeset/polite-llamas-cry.md new file mode 100644 index 00000000000..eb7697716a3 --- /dev/null +++ b/.changeset/polite-llamas-cry.md @@ -0,0 +1,13 @@ +--- +"@salt-ds/core": minor +--- + +Add `padding` and `margin` to `GridLayout`, `GridItem`, `BorderLayout` and `BorderItem` + +```tsx + + + Item + + +``` diff --git a/packages/core/src/border-item/BorderItem.tsx b/packages/core/src/border-item/BorderItem.tsx index bcf94c07eb0..0cd3f4eb343 100644 --- a/packages/core/src/border-item/BorderItem.tsx +++ b/packages/core/src/border-item/BorderItem.tsx @@ -6,6 +6,7 @@ import { GridItem, type GridItemProps } from "../grid-item"; import { type PolymorphicComponentPropWithRef, type PolymorphicRef, + type ResponsiveProp, makePrefixer, } from "../utils"; import borderItemCss from "./BorderItem.css"; @@ -40,6 +41,14 @@ export type BorderItemProps = * Defines if the item should stick to the edges of its container. Defaults to "false" */ sticky?: boolean; + /** + * Defines the margin around the component. It can be specified as a number (which acts as a multiplier) or a string representing the margin value. Default is `0`. + */ + margin?: ResponsiveProp; + /** + * Defines the padding within the component. It can be specified as a number (which acts as a multiplier) or a string representing the padding value. Default is `0`. + */ + padding?: ResponsiveProp; } >; diff --git a/packages/core/src/border-layout/BorderLayout.tsx b/packages/core/src/border-layout/BorderLayout.tsx index abe3930ab93..a33fbd5c4b2 100644 --- a/packages/core/src/border-layout/BorderLayout.tsx +++ b/packages/core/src/border-layout/BorderLayout.tsx @@ -12,6 +12,7 @@ import { GridLayout, type GridLayoutProps } from "../grid-layout"; import { type PolymorphicComponentPropWithRef, type PolymorphicRef, + type ResponsiveProp, makePrefixer, } from "../utils"; @@ -35,6 +36,14 @@ export type BorderLayoutProps = * Border item components to be rendered. */ children: ReactElement>[]; + /** + * Defines the margin around the component. It can be specified as a number (which acts as a multiplier) or a string representing the margin value. Default is `0`. + */ + margin?: ResponsiveProp; + /** + * Defines the padding within the component. It can be specified as a number (which acts as a multiplier) or a string representing the padding value. Default is `0`. + */ + padding?: ResponsiveProp; } >; diff --git a/packages/core/src/flex-item/FlexItem.tsx b/packages/core/src/flex-item/FlexItem.tsx index b81aaa40023..af97d81a553 100644 --- a/packages/core/src/flex-item/FlexItem.tsx +++ b/packages/core/src/flex-item/FlexItem.tsx @@ -8,6 +8,7 @@ import { forwardRef, } from "react"; import { useBreakpoint } from "../breakpoints"; +import { parseSpacing } from "../flex-layout/parseSpacing"; import { type PolymorphicComponentPropWithRef, type PolymorphicRef, @@ -62,13 +63,6 @@ type FlexItemComponent = ( props: FlexItemProps, ) => ReactElement | null; -function parseSpacing(value: number | string | undefined) { - if (value === undefined || typeof value === "string") { - return value; - } - - return `calc(var(--salt-spacing-100) * ${value})`; -} export const FlexItem: FlexItemComponent = forwardRef( ( { diff --git a/packages/core/src/flex-layout/FlexLayout.tsx b/packages/core/src/flex-layout/FlexLayout.tsx index ade0b12e940..9f8c5d5864b 100644 --- a/packages/core/src/flex-layout/FlexLayout.tsx +++ b/packages/core/src/flex-layout/FlexLayout.tsx @@ -11,6 +11,7 @@ import { resolveResponsiveValue, } from "../utils"; import flexLayoutCss from "./FlexLayout.css"; +import { parseSpacing } from "./parseSpacing"; const withBaseName = makePrefixer("saltFlexLayout"); @@ -77,14 +78,6 @@ function parseAlignment(style: string | undefined) { return style === "start" || style === "end" ? `flex-${style}` : style; } -function parseSpacing(value: number | string | undefined) { - if (value === undefined || typeof value === "string") { - return value; - } - - return `calc(var(--salt-spacing-100) * ${value})`; -} - export const FlexLayout: FlexLayoutComponent = forwardRef( ( { diff --git a/packages/core/src/flex-layout/parseSpacing.ts b/packages/core/src/flex-layout/parseSpacing.ts new file mode 100644 index 00000000000..7920902c015 --- /dev/null +++ b/packages/core/src/flex-layout/parseSpacing.ts @@ -0,0 +1,7 @@ +export function parseSpacing(value: number | string | undefined) { + if (value === undefined || typeof value === "string") { + return value; + } + + return `calc(var(--salt-spacing-100) * ${value})`; +} diff --git a/packages/core/src/grid-item/GridItem.css b/packages/core/src/grid-item/GridItem.css index 7bec37f4499..632e54e3647 100644 --- a/packages/core/src/grid-item/GridItem.css +++ b/packages/core/src/grid-item/GridItem.css @@ -6,6 +6,8 @@ grid-column-start: var(--gridItem-gridColumnStart); grid-row-end: var(--gridItem-gridRowEnd); grid-column-end: var(--gridItem-gridColumnEnd); + margin: var(--gridItem-margin); + padding: var(--gridItem-padding); } .saltGridItem-area { diff --git a/packages/core/src/grid-item/GridItem.tsx b/packages/core/src/grid-item/GridItem.tsx index e0e93d648de..6178912e0cc 100644 --- a/packages/core/src/grid-item/GridItem.tsx +++ b/packages/core/src/grid-item/GridItem.tsx @@ -4,6 +4,7 @@ import { type ElementType, type ReactElement, forwardRef } from "react"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import { useBreakpoint } from "../breakpoints"; +import { parseSpacing } from "../flex-layout/parseSpacing"; import { type PolymorphicComponentPropWithRef, type PolymorphicRef, @@ -43,6 +44,14 @@ export type GridItemProps = * Aligns a grid item inside a cell along the block (column) axis. Defaults to "stretch" */ verticalAlignment?: GridAlignment; + /** + * Defines the margin around the component. It can be specified as a number (which acts as a multiplier) or a string representing the margin value. Default is `0`. + */ + margin?: ResponsiveProp; + /** + * Defines the padding within the component. It can be specified as a number (which acts as a multiplier) or a string representing the padding value. Default is `0`. + */ + padding?: ResponsiveProp; } >; @@ -63,6 +72,8 @@ export const GridItem: GridItemComponent = forwardRef( as, children, className, + margin = 0, + padding = 0, colSpan = "auto", rowSpan = "auto", horizontalAlignment = "stretch", @@ -85,7 +96,8 @@ export const GridItem: GridItemComponent = forwardRef( const gridItemColSpan = resolveResponsiveValue(colSpan, matchedBreakpoints); const gridItemRowSpan = resolveResponsiveValue(rowSpan, matchedBreakpoints); - + const gridItemMargin = resolveResponsiveValue(margin, matchedBreakpoints); + const gridItemPadding = resolveResponsiveValue(padding, matchedBreakpoints); const gridColumnStart = gridItemColSpan ? `span ${gridItemColSpan}` : colStart; @@ -97,6 +109,8 @@ export const GridItem: GridItemComponent = forwardRef( const gridRowEnd = gridItemRowSpan ? `span ${gridItemRowSpan}` : rowEnd; const gridStyles = { + "--gridItem-margin": parseSpacing(gridItemMargin), + "--gridItem-padding": parseSpacing(gridItemPadding), ...style, "--gridItem-justifySelf": horizontalAlignment, "--gridItem-alignSelf": verticalAlignment, diff --git a/packages/core/src/grid-layout/GridLayout.css b/packages/core/src/grid-layout/GridLayout.css index e45f4eac221..855422adcd6 100644 --- a/packages/core/src/grid-layout/GridLayout.css +++ b/packages/core/src/grid-layout/GridLayout.css @@ -5,6 +5,8 @@ row-gap: var(--gridLayout-rowGap); grid-template-columns: var(--gridLayout-columns); grid-template-rows: var(--gridLayout-rows); + margin: var(--gridLayout-margin); + padding: var(--gridLayout-padding); grid-auto-columns: auto; grid-auto-rows: auto; } diff --git a/packages/core/src/grid-layout/GridLayout.tsx b/packages/core/src/grid-layout/GridLayout.tsx index 6b263a636a5..d532978bf8f 100644 --- a/packages/core/src/grid-layout/GridLayout.tsx +++ b/packages/core/src/grid-layout/GridLayout.tsx @@ -12,6 +12,7 @@ import { import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import { useBreakpoint } from "../breakpoints"; +import { parseSpacing } from "../flex-layout/parseSpacing"; import gridLayoutCss from "./GridLayout.css"; export type GridLayoutProps = @@ -38,6 +39,14 @@ export type GridLayoutProps = * Defines the size of the gutter between the rows by setting a density multiplier. Defaults to 1 */ rowGap?: ResponsiveProp; + /** + * Defines the margin around the component. It can be specified as a number (which acts as a multiplier) or a string representing the margin value. Default is `0`. + */ + margin?: ResponsiveProp; + /** + * Defines the padding within the component. It can be specified as a number (which acts as a multiplier) or a string representing the padding value. Default is `0`. + */ + padding?: ResponsiveProp; } >; @@ -55,14 +64,6 @@ function parseGridValue(value: number | string | undefined) { return `repeat(${value}, 1fr)`; } -function parseSpacing(value: number | string | undefined) { - if (value === undefined || typeof value === "string") { - return value; - } - - return `calc(var(--salt-spacing-100) * ${value})`; -} - export const GridLayout: GridLayoutComponent = forwardRef( ( { @@ -72,6 +73,8 @@ export const GridLayout: GridLayoutComponent = forwardRef( columns = 12, rows = 1, gap = 3, + margin = 0, + padding = 0, columnGap, rowGap, style, @@ -98,8 +101,11 @@ export const GridLayout: GridLayoutComponent = forwardRef( const gridColumnGap = resolveResponsiveValue(columnGap, matchedBreakpoints); const gridRowGap = resolveResponsiveValue(rowGap, matchedBreakpoints); - + const gridMargin = resolveResponsiveValue(margin, matchedBreakpoints); + const gridPadding = resolveResponsiveValue(padding, matchedBreakpoints); const gridLayoutStyles = { + "--gridLayout-margin": parseSpacing(gridMargin), + "--gridLayout-padding": parseSpacing(gridPadding), ...style, "--gridLayout-columns": parseGridValue(gridColumns), "--gridLayout-rows": parseGridValue(gridRows), diff --git a/packages/core/stories/grid-layout/grid-layout.stories.tsx b/packages/core/stories/grid-layout/grid-layout.stories.tsx index d18579d0284..d8b89b84e5d 100644 --- a/packages/core/stories/grid-layout/grid-layout.stories.tsx +++ b/packages/core/stories/grid-layout/grid-layout.stories.tsx @@ -42,7 +42,30 @@ export const Default = Template.bind({}); Default.args = { columns: { xs: 1, sm: 3, md: 6, lg: 9, xl: 12 }, }; - +const PaddingAndMargins: StoryFn = (args) => { + return ( +
+ + {Array.from({ length: 12 }, (_, index) => ( + +

Item {index + 1}

+
+ ))} +
+
+ ); +}; +export const WithPaddingAndMargins = PaddingAndMargins.bind({}); +WithPaddingAndMargins.args = { + wrap: false, + gap: 1, + padding: 2, + margin: 2, +}; const ResponsiveView: StoryFn = (args) => { return ( diff --git a/packages/core/stories/layout/layout.stories.css b/packages/core/stories/layout/layout.stories.css index 87a44742854..33f81d7b0a9 100644 --- a/packages/core/stories/layout/layout.stories.css +++ b/packages/core/stories/layout/layout.stories.css @@ -131,23 +131,33 @@ animation: fade-out-back 1s ease-out both; } -.spacing-example-gap.saltFlexItem:not(:last-child):after { +.spacing-example-gap.saltFlexItem:not(:last-child):after, +.spacing-example-gap.saltGridItem:not(:last-child):after { content: ""; background: var(--salt-category-5-subtle-background); height: 100%; position: absolute; top: 0; right: 0; + border: dotted 1px var(--salt-category-3-subtle-borderColor); +} +.spacing-example-gap { + position: relative; +} +.spacing-example-gap.saltFlexItem:not(:last-child):after { width: calc(var(--flexLayout-gap) - 2px); transform: translate(calc(var(--flexLayout-gap) + 1px), -1px); - border: dotted 1px var(--salt-category-3-subtle-borderColor); +} +.spacing-example-gap.saltGridItem:not(:last-child):after { + width: calc(var(--gridLayout-rowGap) - 2px); + transform: translate(calc(var(--gridLayout-rowGap) + 1px), -1px); } .spacing-example-gap { border: solid 1px var(--salt-container-primary-borderColor); background: var(--salt-container-primary-background); } .spacing-example-padding { - border: dotted min(1px, var(--flexLayout-padding)) var(--salt-category-3-subtle-borderColor); + border: dotted 1px var(--salt-category-3-subtle-borderColor); background: var(--salt-category-3-subtle-background); } .spacing-example-margin { diff --git a/site/docs/components/border-layout/examples.mdx b/site/docs/components/border-layout/examples.mdx index c542afadd1e..2517fe508df 100644 --- a/site/docs/components/border-layout/examples.mdx +++ b/site/docs/components/border-layout/examples.mdx @@ -73,5 +73,10 @@ Use the `sticky` prop of the border item wrapper to define a region as sticky. W This is particularly useful if you require a region to remain visible at all times. For more information, see [CSS `position: sticky`](https://developer.mozilla.org/en-US/docs/Web/CSS/position#sticky). + + ## Inner and Outer Spacing + The spacing properties `padding` and `margin` are set to 0 by default and can be used to control the layout's inner and outer spacing as multiples of the [Salt base unit](/salt/foundations/spacing). + + diff --git a/site/docs/components/flex-layout/examples.mdx b/site/docs/components/flex-layout/examples.mdx index 273aa703512..83a96268e50 100644 --- a/site/docs/components/flex-layout/examples.mdx +++ b/site/docs/components/flex-layout/examples.mdx @@ -81,7 +81,7 @@ In contrast to the `grow` prop, the `shrink` prop makes a specific item as small ## Spacing Flex layout displays items with a gap between items that is a multiple of the [Salt base unit](/salt/foundations/spacing) controlled by the `gap` property and is by default set to 3. - The spacing properties `padding` and `margin` are set to 0 by default and can be used to control the layout's inner and outer spacing also as multiples of the Salt base unit. + The spacing properties `padding` and `margin` are set to 0 by default and can be used to control the layout's inner and outer spacing (also as multiples of the Salt base unit). diff --git a/site/docs/components/grid-layout/examples.mdx b/site/docs/components/grid-layout/examples.mdx index 4f756648b04..0a49b069411 100644 --- a/site/docs/components/grid-layout/examples.mdx +++ b/site/docs/components/grid-layout/examples.mdx @@ -14,7 +14,7 @@ data: ## Default -`GridLayout` defaults to a 12-column grid with one row. The spacing between rows and columns depends on the prop `gap`, which is 3 by default. +`GridLayout` defaults to a 12-column grid with one row. Content in `GridLayout` is inside `GridItem` components. This helps control the size and position of content. @@ -89,6 +89,14 @@ Use the `verticalAlignment` and `horizontalAlignment` props to control the posit The `columns` and `rows` props allow you to pass a string to define a column or row template. + + + +## Spacing + +Grid layout displays items with a gap between items that is a multiple of the [Salt base unit](/salt/foundations/spacing) controlled by the `gap`, `columnGap` and `rowGap` properties and is by default set to 3. +The spacing properties `padding` and `margin` are set to 0 by default and can be used to control the layout's inner and outer spacing (also as multiples of the Salt base unit). + diff --git a/site/src/examples/border-layout/Spacing.module.css b/site/src/examples/border-layout/Spacing.module.css new file mode 100644 index 00000000000..de0140e5b2a --- /dev/null +++ b/site/src/examples/border-layout/Spacing.module.css @@ -0,0 +1,26 @@ +.spacingExampleGap:not(:last-child):after { + content: ""; + background: var(--salt-category-5-subtle-background); + height: 100%; + position: absolute; + top: 0; + right: 0; + width: calc(var(--gridLayout-rowGap) - 2px); + transform: translate(calc(var(--gridLayout-rowGap) + 1px), -1px); + border: dotted 1px var(--salt-category-3-subtle-borderColor); +} +.item { + position: relative; + border: solid 1px var(--salt-container-primary-borderColor); + background: var(--salt-container-primary-background); + align-items: center; + justify-content: center; +} +.spacingExamplePadding { + border: dotted 1px var(--salt-category-3-subtle-borderColor); + background: var(--salt-category-3-subtle-background); +} +.spacingExampleMargin { + border: dotted 1px var(--salt-category-4-subtle-borderColor); + background: var(--salt-category-4-subtle-background); +} diff --git a/site/src/examples/border-layout/Spacing.tsx b/site/src/examples/border-layout/Spacing.tsx new file mode 100644 index 00000000000..342a3c79c07 --- /dev/null +++ b/site/src/examples/border-layout/Spacing.tsx @@ -0,0 +1,79 @@ +import { + BorderItem, + BorderLayout, + FormField, + FormFieldLabel, + RadioButton, + RadioButtonGroup, + StackLayout, +} from "@salt-ds/core"; +import { clsx } from "clsx"; +import { type ReactElement, useState } from "react"; +import styles from "./Spacing.module.css"; + +export const Spacing = (): ReactElement => { + const [padding, setPadding] = useState(0); + const [margin, setMargin] = useState(0); + + return ( + +
0, + })} + > + 0, + })} + padding={padding} + margin={margin} + > + + North + + + West + + + Center + + + East + + + South + + +
+ + + Padding + setPadding(Number.parseInt(e.target.value))} + direction="horizontal" + value={`${padding}`} + > + + + + + + + + Margin + setMargin(Number.parseInt(e.target.value))} + direction="horizontal" + value={`${margin}`} + > + + + + + + + +
+ ); +}; diff --git a/site/src/examples/border-layout/index.ts b/site/src/examples/border-layout/index.ts index 8193af77894..707b2fd4306 100644 --- a/site/src/examples/border-layout/index.ts +++ b/site/src/examples/border-layout/index.ts @@ -4,4 +4,5 @@ export * from "./GapBetweenRegions"; export * from "./HideRegions"; export * from "./BorderItemPosition"; export * from "./BorderItemAlignment"; +export * from "./Spacing"; export * from "./StickyPositioning"; diff --git a/site/src/examples/grid-layout/Spacing.module.css b/site/src/examples/grid-layout/Spacing.module.css new file mode 100644 index 00000000000..f8573747692 --- /dev/null +++ b/site/src/examples/grid-layout/Spacing.module.css @@ -0,0 +1,24 @@ +.spacingExampleGap:not(:last-child):after { + content: ""; + background: var(--salt-category-5-subtle-background); + height: 100%; + position: absolute; + top: 0; + right: 0; + width: calc(var(--gridLayout-rowGap) - 2px); + transform: translate(calc(var(--gridLayout-rowGap) + 1px), -1px); + border: dotted 1px var(--salt-category-3-subtle-borderColor); +} +.item { + position: relative; + border: solid 1px var(--salt-container-primary-borderColor); + background: var(--salt-container-primary-background); +} +.spacingExamplePadding { + border: dotted 1px var(--salt-category-3-subtle-borderColor); + background: var(--salt-category-3-subtle-background); +} +.spacingExampleMargin { + border: dotted 1px var(--salt-category-4-subtle-borderColor); + background: var(--salt-category-4-subtle-background); +} diff --git a/site/src/examples/grid-layout/Spacing.tsx b/site/src/examples/grid-layout/Spacing.tsx new file mode 100644 index 00000000000..e7130c606fb --- /dev/null +++ b/site/src/examples/grid-layout/Spacing.tsx @@ -0,0 +1,94 @@ +import { + FormField, + FormFieldLabel, + GridItem, + GridLayout, + RadioButton, + RadioButtonGroup, + StackLayout, +} from "@salt-ds/core"; +import { clsx } from "clsx"; +import { type ReactElement, useState } from "react"; +import styles from "./Spacing.module.css"; + +export const Spacing = (): ReactElement => { + const [gap, setGap] = useState(3); + const [padding, setPadding] = useState(0); + const [margin, setMargin] = useState(0); + + return ( + +
0, + })} + > + 0, + })} + columns={5} + gap={gap} + padding={padding} + margin={margin} + > + {Array.from({ length: 5 }, (_, index) => ( + 0, + }, + styles.item, + )} + key={`item-${index + 1}`} + padding={2} + > +

{index + 1}

+
+ ))} +
+
+ + + Gap + setGap(Number.parseInt(e.target.value))} + direction="horizontal" + value={`${gap}`} + > + + + + + + + + Padding + setPadding(Number.parseInt(e.target.value))} + direction="horizontal" + value={`${padding}`} + > + + + + + + + + Margin + setMargin(Number.parseInt(e.target.value))} + direction="horizontal" + value={`${margin}`} + > + + + + + + + +
+ ); +}; diff --git a/site/src/examples/grid-layout/index.ts b/site/src/examples/grid-layout/index.ts index 3f457160328..b49acc572df 100644 --- a/site/src/examples/grid-layout/index.ts +++ b/site/src/examples/grid-layout/index.ts @@ -5,3 +5,4 @@ export * from "./ExpandingAndCollapsingItems"; export * from "./PositioningItems"; export * from "./ResponsiveLayout"; export * from "./SpanningColumnsAndRows"; +export * from "./Spacing";