From 304140e70242107f933a29ad48088445001ec3c0 Mon Sep 17 00:00:00 2001 From: Darren Cadwallader Date: Mon, 11 Dec 2023 11:56:46 +0100 Subject: [PATCH] [#779] Add some layout primitives (stack) (#781) feat: Adds a `Stack` layout primitive https://github.com/bitcrowd/bitstyles/issues/779 --- CHANGELOG.md | 3 +- scss/bitstyles/atoms/_index.scss | 1 + scss/bitstyles/atoms/button/Button.js | 4 +- scss/bitstyles/atoms/stack/Stack.js | 50 ++++++++++++++ scss/bitstyles/atoms/stack/_index.scss | 45 +++++++++++++ scss/bitstyles/atoms/stack/_settings.scss | 11 ++++ scss/bitstyles/atoms/stack/stack.stories.js | 34 ++++++++++ scss/bitstyles/atoms/stack/stack.stories.mdx | 69 ++++++++++++++++++++ test/scss/fixtures/bitstyles-overrides.css | 26 ++++++++ test/scss/fixtures/bitstyles.css | 23 +++++++ test/scss/test-use-all.scss | 9 +++ test/scss/test-use-each.scss | 11 ++++ test/scss/test-use-layers.scss | 9 +++ 13 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 scss/bitstyles/atoms/stack/Stack.js create mode 100644 scss/bitstyles/atoms/stack/_index.scss create mode 100644 scss/bitstyles/atoms/stack/_settings.scss create mode 100644 scss/bitstyles/atoms/stack/stack.stories.js create mode 100644 scss/bitstyles/atoms/stack/stack.stories.mdx diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c542b03..44e1bfcfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Badge with Label, added an example showing a text label rendered next to a badge component, to the badge docs. +- A new layout component at `atoms/switcher`, that lays out its children in a horizontal row with consistent spacing between children. The layout switches to a vertical stack once the width of the component passes below a threshold, or the number of children goes over a limit. +- A new layout component at `atoms/stack`, that lays out its children vertically, with consistent spacing between children. ## [[6.0.0]](https://github.com/bitcrowd/bitstyles/releases/tag/v6.0.0) - 2023-06-08 @@ -46,7 +48,6 @@ - It is now possible to import bitstyles on a per-layer basis, instead of per-module or all at once. It is still possible to override all modules inside each layer in the normal way. - There are now design tokens as `design-tokens/focus` to describe a consistent `:focus` appearance, that are currently used in `base/anchor/`, `atoms/buttons`, and `atoms/links`. - Anchor elements and `atoms/link` components now have a disabled state, applied using the `aria-disabled` attribute. -- A new layout component at `atoms/switcher`, that lays out its children in a horizontal row with consistent spacing between children. The layout switches to a vertical stack once the width of the component passes below a threshold, or the number of children goes over a limit. ## [[5.0.0]](https://github.com/bitcrowd/bitstyles/releases/tag/v5.0.0) - 2023-01-03 diff --git a/scss/bitstyles/atoms/_index.scss b/scss/bitstyles/atoms/_index.scss index 5200c9c6b..9c0af2b6c 100644 --- a/scss/bitstyles/atoms/_index.scss +++ b/scss/bitstyles/atoms/_index.scss @@ -14,4 +14,5 @@ @forward './link' as link-*; @forward './skip-link' as skip-link-*; @forward './switcher' as switcher-*; +@forward './stack' as stack-*; @forward './topbar' as topbar-*; diff --git a/scss/bitstyles/atoms/button/Button.js b/scss/bitstyles/atoms/button/Button.js index 2a323c6ed..4589c0b69 100644 --- a/scss/bitstyles/atoms/button/Button.js +++ b/scss/bitstyles/atoms/button/Button.js @@ -1,4 +1,4 @@ -import { generateLabel } from '../../../../.storybook/helpers'; +import { generateButtonLabel } from '../../../../.storybook/helpers'; export default ({ children, @@ -16,7 +16,7 @@ export default ({ const button = document.createElement(element); button.innerHTML = children || - generateLabel( + generateButtonLabel( shapeVariant, colorVariant, disabled || ariaDisabled, diff --git a/scss/bitstyles/atoms/stack/Stack.js b/scss/bitstyles/atoms/stack/Stack.js new file mode 100644 index 000000000..e4a6cefa4 --- /dev/null +++ b/scss/bitstyles/atoms/stack/Stack.js @@ -0,0 +1,50 @@ +import { generateLabel } from '../../../../.storybook/helpers'; + +const StackItem = ({ + children, + backgroundColor = 'var(--bs-color-grayscale-light-2)', +}) => { + const stackItem = document.createElement('div'); + stackItem.style.backgroundColor = backgroundColor; + stackItem.style.padding = 'var(--bs-content-padding-base)'; + stackItem.style.borderRadius = 'var(--bs-size-s3)'; + stackItem.style.minHeight = '6rem'; + stackItem.innerHTML = children; + return stackItem; +}; + +const Stack = ({ + length = 3, + classname = [], + sizeVariant = '', + itemColor, + labelPrefix = 'stack', + children = [], +}) => { + const stack = document.createElement('div'); + stack.classList.add('a-stack'); + if (sizeVariant) { + stack.classList.add(`a-stack--${sizeVariant}`); + } + + classname.forEach((cls) => { + stack.classList.add(cls); + }); + + if (children.length) { + children.forEach((child) => stack.append(child)); + } else { + for (let child = 0; child < length; child += 1) { + stack.append( + StackItem({ + children: generateLabel([labelPrefix, 'child', child + 1]), + backgroundColor: itemColor, + }) + ); + } + } + + return stack; +}; + +export { StackItem, Stack }; diff --git a/scss/bitstyles/atoms/stack/_index.scss b/scss/bitstyles/atoms/stack/_index.scss new file mode 100644 index 000000000..3ca201850 --- /dev/null +++ b/scss/bitstyles/atoms/stack/_index.scss @@ -0,0 +1,45 @@ +@forward 'settings'; +@use './settings'; +@use '../../tools/classname'; +@use '../../tools/design-token'; +@use '../../tools/media-query'; + +#{classname.get($classname-items: 'stack', $layer: 'atom')} { + display: flex; + flex-direction: column; + justify-content: flex-start; + + &:only-child { + height: 100%; + } + + > * { + margin-top: 0; + margin-bottom: 0; + } + + > * + * { + margin-top: var(design-token.get('stack', 'spacing')); + } +} + +@each $breakpoint, $size-variants in settings.$size-variants { + @include media-query.get($breakpoint) { + @each $size-variant-name, $padding in ($size-variants) { + $class: ''; + @if $size-variant-name == '' { + $class: 'stack'; + } @else { + $class: 'stack--#{$size-variant-name}'; + } + + #{classname.get($classname-items: $class, $layer: 'atom')} { + /* stylelint-disable max-nesting-depth */ + > * + * { + #{design-token.get('stack', 'spacing')}: $padding; + } + /* stylelint-enable max-nesting-depth */ + } + } + } +} diff --git a/scss/bitstyles/atoms/stack/_settings.scss b/scss/bitstyles/atoms/stack/_settings.scss new file mode 100644 index 000000000..bcf99ea20 --- /dev/null +++ b/scss/bitstyles/atoms/stack/_settings.scss @@ -0,0 +1,11 @@ +@use '../../settings/setup'; +@use '../../tools/design-token'; + +$size-variants: ( + '#{setup.$no-media-query}': ( + '': var(design-token.get('content', 'padding', 'base')), + ), + 'm': ( + '': var(design-token.get('content', 'padding', 'l')), + ), +) !default; diff --git a/scss/bitstyles/atoms/stack/stack.stories.js b/scss/bitstyles/atoms/stack/stack.stories.js new file mode 100644 index 000000000..77f247a9c --- /dev/null +++ b/scss/bitstyles/atoms/stack/stack.stories.js @@ -0,0 +1,34 @@ +import { Stack, StackItem } from './Stack'; +import { generateLabel } from '../../../../.storybook/helpers'; + +export default { + title: 'Atoms/Stack', + component: Stack, + argTypes: {}, +}; + +const Template = (args) => Stack(args); + +// ***** Size variants ****************** // + +export const Base = Template.bind({}); +Base.args = { length: 4 }; + +export const Nested = () => { + const innerStack = Stack({ + length: 2, + itemColor: 'var(--bs-color-grayscale-light-4)', + labelPrefix: 'stack 1 child 3 — stack 2', + }); + const children = [ + StackItem({ children: generateLabel(['stack 1', 'child 1']) }), + StackItem({ children: generateLabel(['stack 1', 'child 2']) }), + innerStack, + StackItem({ children: generateLabel(['stack 1', 'child 4']) }), + StackItem({ children: generateLabel(['stack 1', 'child 5']) }), + ]; + const outerStack = Stack({ children }); + + outerStack.insertBefore(innerStack, outerStack.childNodes[2]); + return outerStack; +}; diff --git a/scss/bitstyles/atoms/stack/stack.stories.mdx b/scss/bitstyles/atoms/stack/stack.stories.mdx new file mode 100644 index 000000000..3a1e61068 --- /dev/null +++ b/scss/bitstyles/atoms/stack/stack.stories.mdx @@ -0,0 +1,69 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs'; + + + + + + + +# Stack + +A layout atom that stacks its children vertically and ensures consistent space between each. This component is responsive, applying larger spacing between children when rendered on larger viewports. + +This layout is suitable for large blocks of content, such as the sections in the main content of a page. + +The spacing and the breakpoints the component responds to can be [customized](#customization). You can also add extra size variants of the stack, that apply different spacing, but the default configuration provides only one. + +Stacks can be nested — children of a stack can themselves be stacks — while the spacing will remain consistent: + + + + + +## Customization + +The component expects a Sass list of Sass maps, with the keys being the name of the breakpoint (use `setup.$no-media-query` for the base mobile-first styles) and the values being the name of the stack variant. You can change the breakpoint names or add new breakpoints if you want the component to apply different spacing at extra breakpoints (in which case you probably also want to [edit the available `content` padding design tokens](/docs/design-tokens-content--page) available to you, though you can also pass `size` design tokens directly) + +```scss +@use '~bitstyles/scss/bitstyles/atoms/stack' with ( + $size-variants: ( + '#{setup.$no-media-query}': ( + '': var(design-token.get('content', 'padding', 'base')), + ), + 'm': ( + '': var(design-token.get('content', 'padding', 'l')), + ), + 'l': ( + '': var(design-token.get('content', 'padding', 'xl')), + // this value of content-padding would need to be added to the content design tokens + ), + ) +); +``` + +### Extra size variants + +The keys of the spacing values above are deliberately left blank — that results in those spacing values being applied to the base `a-stack` component. If you provide a key, that will be used to create a stack variant: + +```scss +@use '~bitstyles/scss/bitstyles/atoms/stack' with ( + $size-variants: ( + '#{setup.$no-media-query}': ( + '': var(design-token.get('content', 'padding', 'base')), + 'large': var(design-token.get('content', 'padding', 'l')), + ), + ) +); +``` + +Produces CSS similar to the following: + +```css +.a-stack > * + * { + margin-top: var(--bs-content-padding-base); +} + +.a-stack--large > * + * { + margin-top: var(--bs-content-padding-l); +} +``` diff --git a/test/scss/fixtures/bitstyles-overrides.css b/test/scss/fixtures/bitstyles-overrides.css index 878834c2b..7419f5966 100644 --- a/test/scss/fixtures/bitstyles-overrides.css +++ b/test/scss/fixtures/bitstyles-overrides.css @@ -2055,6 +2055,32 @@ table { --bscpn-switcher-spacing: var(--bscpn-content-padding-l1); } } +.bs-at-stack { + display: flex; + flex-direction: column; + justify-content: flex-start; +} +.bs-at-stack:only-child { + height: 100%; +} +.bs-at-stack > * { + margin-bottom: 0; + margin-top: 0; +} +.bs-at-stack > * + * { + margin-top: var(--bscpn-stack-spacing); +} +.bs-at-stack > * + * { + --bscpn-stack-spacing: var(--bscpn-content-padding-base); +} +.bs-at-stack--large > * + * { + --bscpn-stack-spacing: var(--bscpn-content-padding-l); +} +@media screen and (min-width: 30em) { + .bs-at-stack > * + * { + --bscpn-stack-spacing: var(--bscpn-content-padding-l); + } +} .bs-at-topbar { left: 0; padding: 10rem var(--bscpn-size-s1); diff --git a/test/scss/fixtures/bitstyles.css b/test/scss/fixtures/bitstyles.css index 7d074b47f..3cd61aed2 100644 --- a/test/scss/fixtures/bitstyles.css +++ b/test/scss/fixtures/bitstyles.css @@ -2485,6 +2485,29 @@ table { --bs-switcher-spacing: var(--bs-size-s3); } } +.a-stack { + display: flex; + flex-direction: column; + justify-content: flex-start; +} +.a-stack:only-child { + height: 100%; +} +.a-stack > * { + margin-bottom: 0; + margin-top: 0; +} +.a-stack > * + * { + margin-top: var(--bs-stack-spacing); +} +.a-stack > * + * { + --bs-stack-spacing: var(--bs-content-padding-base); +} +@media screen and (min-width: 30em) { + .a-stack > * + * { + --bs-stack-spacing: var(--bs-content-padding-l); + } +} .a-topbar { left: 0; padding: var(--bs-size-s3) var(--bs-size-s1); diff --git a/test/scss/test-use-all.scss b/test/scss/test-use-all.scss index 5cdf3b4cb..002b139e2 100644 --- a/test/scss/test-use-all.scss +++ b/test/scss/test-use-all.scss @@ -142,6 +142,15 @@ ) ), $icon-sizes: ('s': 10rem), + $stack-size-variants: ( + 'no-mq': ( + '': var(--bscpn-content-padding-base), + 'large': var(--bscpn-content-padding-l), + ), + 'm': ( + '': var(--bscpn-content-padding-l), + ) + ), $skip-link-color: #f00, $switcher-size-variants: ( 'no-mq': ( diff --git a/test/scss/test-use-each.scss b/test/scss/test-use-each.scss index 986c49fa5..3950b6aac 100644 --- a/test/scss/test-use-each.scss +++ b/test/scss/test-use-each.scss @@ -251,6 +251,17 @@ ), ) ); +@use '../../scss/bitstyles/atoms/stack' with ( + $size-variants: ( + 'no-mq': ( + '': var(--bscpn-content-padding-base), + 'large': var(--bscpn-content-padding-l), + ), + 'm': ( + '': var(--bscpn-content-padding-l), + ), + ) +); @use '../../scss/bitstyles/atoms/topbar' with ( $vertical-padding: 10rem ); diff --git a/test/scss/test-use-layers.scss b/test/scss/test-use-layers.scss index 987c0eacf..b5c5466e1 100644 --- a/test/scss/test-use-layers.scss +++ b/test/scss/test-use-layers.scss @@ -172,6 +172,15 @@ ), ), ), + $stack-size-variants: ( + 'no-mq': ( + '': var(--bscpn-content-padding-base), + 'large': var(--bscpn-content-padding-l), + ), + 'm': ( + '': var(--bscpn-content-padding-l), + ), + ), $topbar-vertical-padding: 10rem ); @use '../../scss/bitstyles/organisms' with (