Skip to content

Commit

Permalink
[#779] Add some layout primitives (switcher) (#784)
Browse files Browse the repository at this point in the history
feat: New switcher layout primitive

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..

`content-padding` design tokens are now using the same naming scheme as the base size tokens (`l1`, `s1` etc)

#779
  • Loading branch information
planktonic authored Jul 20, 2023
1 parent 1870def commit 45bc652
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 12 deletions.
4 changes: 3 additions & 1 deletion .storybook/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

export const generateLabel = (
export const generateLabel = (items) => capitalizeFirstLetter(items.join(' '));

export const generateButtonLabel = (
shapeVariant = [],
colorVariant = [],
disabled = false,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- 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

Expand Down
1 change: 1 addition & 0 deletions scss/bitstyles/atoms/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
@forward './icon' as icon-*;
@forward './link' as link-*;
@forward './skip-link' as skip-link-*;
@forward './switcher' as switcher-*;
@forward './topbar' as topbar-*;
2 changes: 1 addition & 1 deletion scss/bitstyles/atoms/content/_settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ $max-width-base: 'm' !default;
$padding: (
'#{setup.$no-media-query}':
var(design-token.get('content', 'padding', 'base')),
'l': var(design-token.get('content', 'padding', 'l')),
'l': var(design-token.get('content', 'padding', 'l1')),
) !default;
35 changes: 35 additions & 0 deletions scss/bitstyles/atoms/switcher/Switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { generateLabel } from '../../../../.storybook/helpers';

const SwitcherItem = ({
children,
backgroundColor = 'var(--bs-color-grayscale-light-2)',
}) => {
const switcherItem = document.createElement('div');
switcherItem.style.backgroundColor = backgroundColor;
switcherItem.style.paddingBlock = 'var(--bs-size-s2)';
switcherItem.style.paddingInline = 'var(--bs-content-padding-base)';
switcherItem.style.borderRadius = 'var(--bs-size-s4)';
switcherItem.innerHTML = children;
return switcherItem;
};

const Switcher = ({ length = 3, classname = [], children = [] }) => {
const switcher = document.createElement('div');
switcher.classList.add('a-switcher');
classname.forEach((cls) => switcher.classList.add(cls));

if (children.length) {
children.forEach((child) => switcher.append(child));
} else {
for (let child = 0; child < length; child += 1) {
switcher.append(
SwitcherItem({
children: generateLabel(['switcher', 'child', child + 1]),
})
);
}
}
return switcher;
};

export { Switcher, SwitcherItem };
54 changes: 54 additions & 0 deletions scss/bitstyles/atoms/switcher/_index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@forward 'settings';
@use './settings';
@use '../../tools/classname';
@use '../../tools/design-token';
@use '../../tools/media-query';
@use 'sass:map';

/* stylelint-disable scss/dollar-variable-default */
$breakpoint-property-name: design-token.get('switcher', 'breakpoint');
$spacing-property-name: design-token.get('switcher', 'spacing');

#{classname.get($classname-items: 'switcher', $layer: 'atom')} {
display: flex;
flex-wrap: wrap;
gap: var(#{$spacing-property-name});

> * {
flex-basis: calc((var(#{$breakpoint-property-name}) - 100%) * 999);
flex-grow: 1;
}
}
/* stylelint-enable scss/dollar-variable-default */

@each $breakpoint, $size-variants in settings.$size-variants {
@include media-query.get($breakpoint) {
@each $size-variant-name, $size-variant in ($size-variants) {
$class: '';
@if $size-variant-name == '' {
$class: 'switcher';
} @else {
$class: 'switcher--#{$size-variant-name}';
}

/* stylelint-disable max-nesting-depth */
#{classname.get($classname-items: $class, $layer: 'atom')} {
@if map.has-key($size-variant, 'breakpoint') {
#{$breakpoint-property-name}: #{map.get($size-variant, 'breakpoint')};
}

@if map.has-key($size-variant, 'spacing') {
#{$spacing-property-name}: #{map.get($size-variant, 'spacing')};
}

@if map.has-key($size-variant, 'limit') {
> :nth-last-child(n + #{map.get($size-variant, 'limit') + 1}),
> :nth-last-child(n + #{map.get($size-variant, 'limit') + 1}) ~ * {
flex-basis: 100%;
}
}
}
/* stylelint-enable max-nesting-depth */
}
}
}
17 changes: 17 additions & 0 deletions scss/bitstyles/atoms/switcher/_settings.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use '../../settings/setup';
@use '../../tools/design-token';

$size-variants: (
'#{setup.$no-media-query}': (
'': (
'spacing': var(design-token.get('size', 's5')),
'breakpoint': 30rem,
'limit': 4,
),
),
'l': (
'': (
'spacing': var(design-token.get('size', 's3')),
),
),
) !default;
26 changes: 26 additions & 0 deletions scss/bitstyles/atoms/switcher/switcher.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Switcher } from './Switcher';

export default {
title: 'Atoms/Switcher',
component: Switcher,
argTypes: {},
};

const Template = (args) => Switcher(args);

// ***** Size variants ****************** //

export const One = Template.bind({});
One.args = { length: 1 };

export const Two = Template.bind({});
Two.args = { length: 2 };

export const Three = Template.bind({});
Three.args = { length: 3 };

export const Four = Template.bind({});
Four.args = { length: 4 };

export const Five = Template.bind({});
Five.args = { length: 5 };
99 changes: 99 additions & 0 deletions scss/bitstyles/atoms/switcher/switcher.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Canvas, Meta, Story } from '@storybook/addon-docs';

<Meta title="Atoms/Switcher/Overview" />

<Canvas>
<Story id="atoms-switcher--three" />
</Canvas>

# Switcher

A layout atom that places its children in a single horizontal row and ensures consistent space between each. The layout switches to a vertical stack when it is less than `30rem` wide, or when there are more than 4 children (with default configuration). All children will be the same width in both layouts. This component is responsive, applying larger spacing between children when rendered on larger viewports.

The layout is best suited to small blocks of content such as a list of related buttons or stats.

The spacing and the breakpoints the component responds to can be [customized](#customization). You can also add extra size variants of the switcher that apply different spacing; the default configuration provides one size variant that applies a larger spacing at the `l` breakpoint.

<Canvas>
<Story id="atoms-switcher--one" />
</Canvas>

<Canvas>
<Story id="atoms-switcher--two" />
</Canvas>

<Canvas>
<Story id="atoms-switcher--three" />
</Canvas>

<Canvas>
<Story id="atoms-switcher--four" />
</Canvas>

When a switcher has more children than the specified limit (4 in the default configuration), it will switch to the vertical layout, to avoid the children being squashed.

<Canvas>
<Story id="atoms-switcher--five" />
</Canvas>

## 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 map with the key being the name of the switcher variant, and the value a list of properties. 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/switcher' with (
$size-variants: (
'#{setup.$no-media-query}': (
'': (
'spacing': var(design-token.get('size', 's5')),
'breakpoint': 30rem,
'limit': 4,
),
),
'l': (
'': (
'spacing': var(design-token.get('size', 's3')),
),
),
)
);
```

### Available properties

| Property | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `spacing` | Spacing between each child. This value is used in both horizontal and vertical layouts. |
| `breakpoint` | Minimum inline-size of the row layout, below which it switches to the stack layout. |
| `limit` | The maximum number of children that will fit in the row. If there are more children, the switcher will render a stack layout. |

### New size variants

The keys of the variants values above are deliberately left blank — that results in those properties being applied to the base `a-stack` component. If you provide a key, that will be used to create a stack variant. See the example below, where a `tight` variant is created with no spacing

```scss
@use '~bitstyles/scss/bitstyles/atoms/switcher' with (
$size-variants: (
'#{setup.$no-media-query}': (
'': (
'spacing': var(design-token.get('size', 's5')),
),
'tight': (
'spacing': 0,
),
),
)
);
```

Produces CSS similar to the following:

```css
.a-switcher {
gap: var(--bs-size-s5);
}

.a-switcher--tight {
gap: 0;
}
```
4 changes: 2 additions & 2 deletions scss/bitstyles/design-tokens/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
$padding: (
'#{setup.$no-media-query}': (
'base': var(design-token.get('size', 's1')),
'l': var(design-token.get('size', 'm')),
'l1': var(design-token.get('size', 'm')),
),
'm': (
'base': var(design-token.get('size', 'l1')),
'l': var(design-token.get('size', 'l1')),
'l1': var(design-token.get('size', 'l3')),
),
) !default;

Expand Down
28 changes: 24 additions & 4 deletions test/scss/fixtures/bitstyles-overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@
}
:root {
--bscpn-content-padding-base: var(--bscpn-size-s1);
--bscpn-content-padding-l: var(--bscpn-size-m);
--bscpn-content-padding-l1: var(--bscpn-size-m);
}
@media screen and (min-width: 30em) {
:root {
--bscpn-content-padding-base: var(--bscpn-size-l1);
--bscpn-content-padding-l: var(--bscpn-size-l1);
--bscpn-content-padding-l1: var(--bscpn-size-l3);
}
}
html {
Expand Down Expand Up @@ -1786,8 +1786,8 @@ table {
}
@media screen and (min-width: 55em) {
.bs-at-content {
padding-left: var(--bscpn-content-padding-l);
padding-right: var(--bscpn-content-padding-l);
padding-left: var(--bscpn-content-padding-l1);
padding-right: var(--bscpn-content-padding-l1);
}
}
.bs-at-content--xs {
Expand Down Expand Up @@ -2035,6 +2035,26 @@ table {
calc(var(--bscpn-size-s7) + var(--bscpn-size-s7) / 2);
outline-offset: var(--bscpn-size-s7);
}
.bs-at-switcher {
display: flex;
flex-wrap: wrap;
gap: var(--bscpn-switcher-spacing);
}
.bs-at-switcher > * {
flex-basis: calc((var(--bscpn-switcher-breakpoint) - 100%) * 999);
flex-grow: 1;
}
.bs-at-switcher {
--bscpn-switcher-spacing: var(--bscpn-content-padding-base);
}
.bs-at-switcher--large {
--bscpn-switcher-spacing: var(--bscpn-content-padding-l1);
}
@media screen and (min-width: 30em) {
.bs-at-switcher {
--bscpn-switcher-spacing: var(--bscpn-content-padding-l1);
}
}
.bs-at-topbar {
left: 0;
padding: 10rem var(--bscpn-size-s1);
Expand Down
30 changes: 26 additions & 4 deletions test/scss/fixtures/bitstyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@
}
:root {
--bs-content-padding-base: var(--bs-size-s1);
--bs-content-padding-l: var(--bs-size-m);
--bs-content-padding-l1: var(--bs-size-m);
}
@media screen and (min-width: 30em) {
:root {
--bs-content-padding-base: var(--bs-size-l1);
--bs-content-padding-l: var(--bs-size-l1);
--bs-content-padding-l1: var(--bs-size-l3);
}
}
html {
Expand Down Expand Up @@ -2198,8 +2198,8 @@ table {
}
@media screen and (min-width: 55em) {
.a-content {
padding-left: var(--bs-content-padding-l);
padding-right: var(--bs-content-padding-l);
padding-left: var(--bs-content-padding-l1);
padding-right: var(--bs-content-padding-l1);
}
}
.a-content--xs {
Expand Down Expand Up @@ -2463,6 +2463,28 @@ table {
calc(var(--bs-size-s7) + var(--bs-size-s7) / 2);
outline-offset: var(--bs-size-s7);
}
.a-switcher {
display: flex;
flex-wrap: wrap;
gap: var(--bs-switcher-spacing);
}
.a-switcher > * {
flex-basis: calc((var(--bs-switcher-breakpoint) - 100%) * 999);
flex-grow: 1;
}
.a-switcher {
--bs-switcher-breakpoint: 30rem;
--bs-switcher-spacing: var(--bs-size-s5);
}
.a-switcher > :nth-last-child(n + 5),
.a-switcher > :nth-last-child(n + 5) ~ * {
flex-basis: 100%;
}
@media screen and (min-width: 55em) {
.a-switcher {
--bs-switcher-spacing: var(--bs-size-s3);
}
}
.a-topbar {
left: 0;
padding: var(--bs-size-s3) var(--bs-size-s1);
Expand Down
Loading

0 comments on commit 45bc652

Please sign in to comment.