From 982b76f75218b1148d6a2e41914a66b0324e2760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= Date: Wed, 18 Sep 2024 12:36:56 +0200 Subject: [PATCH] feat(styles): create spacing utilities from tokens --- .changeset/proud-actors-knock.md | 6 + .../snapshots/utilities/spacing.snapshot.ts | 13 + .../utilities/spacing/spacing.docs.mdx | 81 +++--- .../utilities/spacing/spacing.module.scss | 12 +- .../spacing/spacing.snapshot.stories.ts | 42 +++ .../utilities/spacing/spacing.stories.ts | 260 +++++++++--------- .../utilities/spacing/spacing.styles.scss | 59 +++- packages/styles/src/basics.scss | 5 +- packages/styles/src/components/_index.scss | 1 - packages/styles/src/components/sizing.scss | 35 +-- packages/styles/src/functions/_string.scss | 13 + packages/styles/src/index.scss | 1 + packages/styles/src/intranet.scss | 3 +- packages/styles/src/post-external.scss | 1 + packages/styles/src/post-internal.scss | 1 + .../src/themes/bootstrap/_utilities.scss | 20 ++ packages/styles/src/utilities/_mixins.scss | 23 ++ packages/styles/src/utilities/_variables.scss | 86 ++++++ packages/styles/src/utilities/index.scss | 30 ++ .../temp/_legacy.scss} | 16 +- .../tests/components/utilities.test.scss | 7 - .../styles/tests/functions/string.test.scss | 22 ++ .../styles/tests/utilities/index.test.scss | 16 ++ .../styles/tests/utilities/mixins.test.scss | 11 + 24 files changed, 534 insertions(+), 230 deletions(-) create mode 100644 .changeset/proud-actors-knock.md create mode 100644 packages/documentation/cypress/snapshots/utilities/spacing.snapshot.ts create mode 100644 packages/documentation/src/stories/utilities/spacing/spacing.snapshot.stories.ts create mode 100644 packages/styles/src/functions/_string.scss create mode 100644 packages/styles/src/utilities/_mixins.scss create mode 100644 packages/styles/src/utilities/_variables.scss create mode 100644 packages/styles/src/utilities/index.scss rename packages/styles/src/{components/utilities.scss => utilities/temp/_legacy.scss} (79%) delete mode 100644 packages/styles/tests/components/utilities.test.scss create mode 100644 packages/styles/tests/functions/string.test.scss create mode 100644 packages/styles/tests/utilities/index.test.scss create mode 100644 packages/styles/tests/utilities/mixins.test.scss diff --git a/.changeset/proud-actors-knock.md b/.changeset/proud-actors-knock.md new file mode 100644 index 0000000000..96980d3ac4 --- /dev/null +++ b/.changeset/proud-actors-knock.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-styles': major +'@swisspost/design-system-documentation': minor +--- + +Updated the margin, padding, and gap utility classes to use the pixel values (1, 2, ... , 112) instead of size names (hair, line, ..., bigger-giant). diff --git a/packages/documentation/cypress/snapshots/utilities/spacing.snapshot.ts b/packages/documentation/cypress/snapshots/utilities/spacing.snapshot.ts new file mode 100644 index 0000000000..ad8f398489 --- /dev/null +++ b/packages/documentation/cypress/snapshots/utilities/spacing.snapshot.ts @@ -0,0 +1,13 @@ +describe('Spacing', () => { + it('margin and padding', () => { + cy.visit('/iframe.html?id=snapshots--margin-and-padding'); + cy.get('.margin-padding-example', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('Margin and Padding', { widths: [320, 1440] }); + }); + + it('gap', () => { + cy.visit('/iframe.html?id=snapshots--gap'); + cy.get('.gap-example', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('Gap', { widths: [320, 1440] }); + }); +}); diff --git a/packages/documentation/src/stories/utilities/spacing/spacing.docs.mdx b/packages/documentation/src/stories/utilities/spacing/spacing.docs.mdx index ecd61e9877..b6d2cb7ed9 100644 --- a/packages/documentation/src/stories/utilities/spacing/spacing.docs.mdx +++ b/packages/documentation/src/stories/utilities/spacing/spacing.docs.mdx @@ -1,63 +1,76 @@ import { Canvas, Controls, Meta } from '@storybook/blocks'; import * as SpacingStories from './spacing.stories'; +export const firstBreakpoint = Object.values(SpacingStories.SCSS_VARIABLES.firstBreakpoint)[0]; +export const breakpoints = Object.values(SpacingStories.SCSS_VARIABLES.breakpoints).map((breakpoint, i, arr) => [breakpoint, i !== arr.length - 1]); + # Spacing
-Our spacing utility brings uniform and consistent spacing to your elements, +Ensure consistent spacing across all pages. +
-By adhering to standardized spacing guidelines, we maintain visual alignment and -improve the overall user interface. +## Margin and Padding +
+

Avoid applying spacing classes to grid elements (e.g. `.row`, `.col`, `.col-*`).

+

The grid system uses negative margins and positive padding along the horizontal axis, and applying additional spacing classes can affect the layout unless done with careful consideration.

-
-

Sizing variables are deprecated

-

The current set of the post-specific spacing utility is deprecated in favour of a new naming system that is consistent with the Design. For further information, please read the discussion on sizing variables on GitHub and have a look at the implementation in Figma.

-

There is a new solution with updated naming system up coming for spacing sizes.

-
+The naming convention for spacing utilities follows this pattern: +- For all breakpoints ({firstBreakpoint} and up): `{property}{sides}-{size}` +- Starting from a specific breakpoint ({breakpoints.map(([b, isLast]) => ({b}{isLast ? ', ' : ''}))}): `{property}{sides}-{breakpoint}-{size}` -## Padding & Margin +### Properties -You can apply the desired spacings for `margin` and/or `padding` all around an element using the classes `m-*` and `p-*`, or you can specify a position (e.g. `pt-*` , `pb-*` , `pe-*` , `ps-*` respectively for a padding at top, bottom, end/right and start/left). You can also set a spacing along the horizontal axis (i.e. right and left) using `mx-*` / `px-*` or along the vertical axis (i.e. top and bottom) using `my-*` / `py-*` . +- `m`: for classes that set margin +- `p`: for classes that set padding -Our base spacing classes with suffixes going from 0 to 5 (`*-0`, `*-1`, ..., `*-5`). +### Sides -Our custom sizes can be used in the same way: by adding the name of the desired size to a prefix. See our [sizing docs](/?path=/docs/e728de1f-0d71-4317-8bb8-cbef0bf8d5db--docs) to find out which size names are available. +- blank: for classes that set a margin or padding on all sides (top, bottom, left, and right) +- `x`: for classes that set a horizontal margin or padding (left and right) +- `y`: for classes that set a vertical margin or padding (top and bottom) +- `t`: for classes that set a margin or padding at the top +- `b`: for classes that set a margin or padding at the bottom +- `s`: for classes that set a margin or padding at the start (left in LTR) +- `e`: for classes that set a margin or padding at the end (right in LTR) -
-

Do not use spacing classes on grid elements (i.e. `.row`, `.col`, `.col-*`).

-

Our grid is built with negative margins and positive paddings on the x-axis. Therefore, use this spacing classes only if you know exactly what you are doing!

-
+### Sizes + +See all available sizes in the example bellow. - +### Example + +
- +
-## Responsive behavior - -By default, the above classes apply to all breakpoints. +## Gap -##### Manually specifying breakpoints +When using `display: grid` or `display: flex`, you can use the gap utilities to manage spacing between child elements. +These classes simplify the process of managing spacing without needing to individually add margin utilities to each child element. -If you need to change the size or spacing of an element based on the breakpoint, you should -mention it in the class name using one of the following infixes: {Object.values(SpacingStories.SCSS_VARIABLES.breakpoints).map((s, index, array) => ({`-${s}-`}{index !== array.length - 1 && ', '}))}. -When a breakpoint is specified, the size applies to that breakpoint and to all those that are -larger. +The format for gap classes is as follows: +- For all breakpoints ({firstBreakpoint} and up): `{property}-{size}` +- Starting from a specific breakpoint ({breakpoints.map(([b, isLast]) => ({b}{isLast ? ', ' : ''}))}): `gap-{breakpoint}-{size}` -The square below has a big padding from the large breakpoint (i.e. `.p-lg-big`) and a regular -padding below (i.e. `.p-regular`). +### Properties - +- `row-gap`: for classes that set a gap between rows +- `column-gap`: for classes that set a gap between columns +- `gap`: for classes that set a gap between both columns and rows -##### Using automatic responsive behavior +### Sizes -Another way to define a responsive size is to use the `-r` suffix. It allows to obtain a size which updates automatically and consistently depending on the breakpoint, without having to specify anything manually. +See all available sizes in the example bellow. -The square below has a "large" responsive padding, which means the padding size -automatically changes based on the breakpoint but remains visually consistent. +### Example - + +
+ +
diff --git a/packages/documentation/src/stories/utilities/spacing/spacing.module.scss b/packages/documentation/src/stories/utilities/spacing/spacing.module.scss index 05a8bec5eb..0b5a6608cb 100644 --- a/packages/documentation/src/stories/utilities/spacing/spacing.module.scss +++ b/packages/documentation/src/stories/utilities/spacing/spacing.module.scss @@ -1,12 +1,18 @@ @use 'sass:list'; @use 'sass:map'; @use '@swisspost/design-system-styles/core' as post; +@use '@swisspost/design-system-styles/tokens/utilities' as tokens; :export { - @each $key, $value in post.$post-sizes { - sizes_#{$key}: #{$value}; + @each $key, $value in tokens.$post-spacing { + spacing_#{$key}: #{$value}; } @each $breakpoint in post.$grid-breakpoints-list { - breakpoints_#{$breakpoint}: $breakpoint; + @if (map.get(post.$grid-breakpoints, $breakpoint) == 0) { + firstBreakpoint_#{$breakpoint}: $breakpoint; + } + @if (map.get(post.$grid-breakpoints, $breakpoint) != 0) { + breakpoints_#{$breakpoint}: $breakpoint; + } } } diff --git a/packages/documentation/src/stories/utilities/spacing/spacing.snapshot.stories.ts b/packages/documentation/src/stories/utilities/spacing/spacing.snapshot.stories.ts new file mode 100644 index 0000000000..ae8276ebb8 --- /dev/null +++ b/packages/documentation/src/stories/utilities/spacing/spacing.snapshot.stories.ts @@ -0,0 +1,42 @@ +import type { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import meta, { MarginAndPadding as MandP, Gap as G } from './spacing.stories'; +import './spacing.styles.scss'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const MarginAndPadding: Story = { + render: (_args: Args, context: StoryContext) => { + const snapshotArgs = { + marginSize: '24', + paddingSize: '16', + breakpointClasses: 'm-md-48 p-md-32', + }; + + return html` +
+ ${MandP.render?.({ ...MandP.args, ...snapshotArgs }, context)} +
+ `; + }, +}; + +export const Gap: Story = { + render: (_args: Args, context: StoryContext) => { + const snapshotArgs = { + gapSize: '24', + breakpointClass: 'gap-md-48', + }; + + return html` +
${G.render?.({ ...G.args, ...snapshotArgs }, context)}
+ `; + }, +}; diff --git a/packages/documentation/src/stories/utilities/spacing/spacing.stories.ts b/packages/documentation/src/stories/utilities/spacing/spacing.stories.ts index 16aaeab012..727dae4e5f 100644 --- a/packages/documentation/src/stories/utilities/spacing/spacing.stories.ts +++ b/packages/documentation/src/stories/utilities/spacing/spacing.stories.ts @@ -1,179 +1,187 @@ import type { Args, StoryContext, StoryFn, StoryObj } from '@storybook/web-components'; +import { TemplateResult } from 'lit'; import { html } from 'lit/static-html.js'; -import './spacing.styles.scss'; +import { MetaExtended } from '@root/types'; import { parse } from '@/utils/sass-export'; import scss from './spacing.module.scss'; -import { MetaExtended } from '@root/types'; +import './spacing.styles.scss'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export const SCSS_VARIABLES: any = parse(scss); -const sizingOptions = ['0', '1', '2', '3', '4', '5', 'auto', ...Object.keys(SCSS_VARIABLES.sizes)]; +const properties = ['margin', 'padding', 'gap']; +const sizes = properties.reduce((options, property) => { + return { + ...options, + [property]: Object.keys(SCSS_VARIABLES.spacing) + .filter((key: string) => key.startsWith(`post-utility-${property}-`)) + .map((key: string) => key.replace(`post-utility-${property}-`, '')), + }; +}, {} as { [property: string]: string[] }); -const positionOptions = { - null: 'All around', - x: 'Along the horizontal axis', - y: 'Along the vertical axis', - t: 'At the top', - b: 'At the bottom', - e: 'To the right', - s: 'To the left', +const sides = { + null: 'All sides', + x: 'x - Along the horizontal axis', + y: 'y - Along the vertical axis', + t: 't - At the top', + b: 'b - At the bottom', + s: 's - At the start', + e: 'e - At the end', }; const meta: MetaExtended = { id: 'facaacfd-18f1-49b4-80f1-a96680730fa0', title: 'Utilities/Spacing', - parameters: { - badges: [], - }, - args: { - marginSize: 'regular', - marginPosition: 'null', - paddingSize: 'regular', - paddingPosition: 'null', - }, +}; + +export default meta; + +type Story = StoryObj; + +function withLegend(template: TemplateResult, ...legendItems: string[]) { + legendItems.unshift('element'); + return html` +
+ ${template} +
    + ${legendItems.map( + item => html` +
  • +
    + ${item} +
  • + `, + )} +
+
+ `; +} + +export const MarginAndPadding: Story = { argTypes: { - marginSize: { - name: 'Margin size', - description: 'Sets the size of the Margin.', + marginSides: { + name: 'Sides', + description: 'Sets the margin sides.', control: { type: 'select', + labels: sides, }, - options: sizingOptions, + options: Object.keys(sides), table: { - category: 'General', + category: 'Margin', }, }, - marginPosition: { - name: 'Margin Position', - description: 'Sets the position of the Margin.', + marginSize: { + name: 'Size', + description: 'Sets the margin size.', control: { type: 'select', - labels: positionOptions, }, - options: Object.keys(positionOptions), + options: sizes.margin, table: { - category: 'General', + category: 'Margin', }, }, - paddingSize: { - name: 'Padding size', - description: 'Sets the size of the Padding.', + paddingSides: { + name: 'Sides', + description: 'Sets the padding sides.', control: { type: 'select', + labels: sides, }, - options: sizingOptions.filter(option => option !== 'auto'), + options: Object.keys(sides), table: { - category: 'General', + category: 'Padding', }, }, - paddingPosition: { - name: 'Padding Position', - description: 'Sets the position of the Padding.', + paddingSize: { + name: 'Size', + description: 'Sets the padding size.', control: { type: 'select', - labels: positionOptions, }, - options: Object.keys(positionOptions), + options: sizes.padding, table: { - category: 'General', + category: 'Padding', }, }, }, -}; - -export default meta; + args: { + marginSides: 'null', + marginSize: '20', + paddingSides: 'null', + paddingSize: '12', + }, + render: (args: Args) => { + const getPosition = (position: string) => (position === 'null' ? '' : position); + const marginClass = `m${getPosition(args.marginSides)}-${args.marginSize}`; + const paddingClass = `p${getPosition(args.paddingSides)}-${args.paddingSize}`; -type Story = StoryObj; + // used only for the snapshots + const breakpointClasses = args.breakpointClasses ? ` ${args.breakpointClasses}` : ''; -export const Default: Story = { - render: (args: Args) => { - // this will be the only code visible in the code preview - return html` -
- `; + return html`
`; }, decorators: [ - // everything in here will be visible in the example, but only the content coming from the `story` function will be shown in the code preview - (story: StoryFn, { args, context }: StoryContext) => html` -
-
${story(args, context)}
- -
-
-
-
-
-
-
- -
    -
  • -
    - margin -
  • -
  • -
    - padding -
  • -
  • -
    - content -
  • -
-
- `, + (story: StoryFn, context: StoryContext) => { + const storyTemplate = html` +
${story(context.args, context)}
+ `; + return withLegend(storyTemplate, 'margin', 'padding'); + }, ], }; -export const ResponsiveExample: Story = { - render: () => { - return html`
`; +export const Gap: Story = { + argTypes: { + property: { + name: 'Property', + description: 'Sets the gap property.', + control: { + type: 'select', + }, + options: ['gap', 'row-gap', 'column-gap'], + table: { + category: 'Gap', + }, + }, + size: { + name: 'Size', + description: 'Sets the gap size.', + control: { + type: 'select', + }, + options: sizes.gap, + table: { + category: 'Gap', + }, + }, }, - decorators: [ - // everything in here will be visible in the example, but only the content coming from the `story` function will be shown in the code preview - (story: StoryFn, { args, context }: StoryContext) => html` -
-
${story(args, context)}
-
-
-
-

Resize the browser window to see changes.

+ args: { + property: 'gap', + size: '16', + }, + render: (args: Args) => { + // used only for the snapshots + const breakpointClass = args.breakpointClass ? ` ${args.breakpointClass}` : ''; + return html` +
+
First child
+
Second child
+
Third child
+
Fourth child
+
Fifth child
+
Sixth child
- `, - ], -}; - -export const AutomaticResponsiveExample: Story = { - render: () => { - return html`
`; + `; }, decorators: [ - (story: StoryFn, { args, context }: StoryContext) => html` -
-
${story(args, context)}
-
-
-
-

Resize the browser window to see changes.

-
- `, + (story: StoryFn, context: StoryContext) => { + const storyTemplate = html`
${story(context.args, context)}
`; + return withLegend(storyTemplate, 'gap'); + }, ], }; diff --git a/packages/documentation/src/stories/utilities/spacing/spacing.styles.scss b/packages/documentation/src/stories/utilities/spacing/spacing.styles.scss index 4e6328d3c1..f9afc38bd2 100644 --- a/packages/documentation/src/stories/utilities/spacing/spacing.styles.scss +++ b/packages/documentation/src/stories/utilities/spacing/spacing.styles.scss @@ -1,24 +1,55 @@ +@use 'sass:color'; @use '@swisspost/design-system-styles/core' as post; -.spacing-example { +$margin-color: color.adjust(post.$coral-bright, $lightness: 30%); +$padding-color: color.complement($margin-color); +$gap-color: color.adjust(post.$aubergine-bright, $lightness: 30%); +$content-color: post.$white; +$border: 1px solid post.$gray-40; + +.margin-padding-example { + display: flex; + width: fit-content; + background-color: $margin-color; + + > div { + height: 100px; + width: 100px; + background-image: linear-gradient($content-color, $content-color), linear-gradient($padding-color, $padding-color); + background-clip: content-box, padding-box; + border: $border; + } +} + +.gap-example { + > div { + width: fit-content; + background-color: $gap-color; + border: $border; + text-align: center; + + > div { + background-color: $content-color; + padding: 12px 16px; + } + } +} + +.legend { .margin { - background-color: #f4cea3; + background-color: $margin-color; } + .padding { - background-color: #c4dab9; - border: 1px solid post.$gray-40; - } - .content { - background-color: post.$white; + background-color: $padding-color; } - .legend { - .padding { - border: 0 none; - } + .gap { + background-color: $gap-color; + } - .content { - border: 1px solid post.$gray-40; - } + .element { + background-color: $content-color; + border: $border; } } diff --git a/packages/styles/src/basics.scss b/packages/styles/src/basics.scss index 065e4439cb..b89c93e1f3 100644 --- a/packages/styles/src/basics.scss +++ b/packages/styles/src/basics.scss @@ -1,7 +1,8 @@ @forward './variables/options'; -@use './elements'; +@use 'elements'; +@use 'utilities'; + @use 'components/elevation'; -@use 'components/utilities'; @use 'components/sizing'; @use 'components/grid'; diff --git a/packages/styles/src/components/_index.scss b/packages/styles/src/components/_index.scss index 21c9c85e48..60ccbcc3fb 100644 --- a/packages/styles/src/components/_index.scss +++ b/packages/styles/src/components/_index.scss @@ -38,7 +38,6 @@ @use 'tooltip'; @use 'topic-teaser'; @use 'transitions'; -@use 'utilities'; @use 'tag'; // Imports depending on source order to override bootstrap styles diff --git a/packages/styles/src/components/sizing.scss b/packages/styles/src/components/sizing.scss index 68bf75e297..228e836d8e 100644 --- a/packages/styles/src/components/sizing.scss +++ b/packages/styles/src/components/sizing.scss @@ -6,47 +6,16 @@ @use './../variables/breakpoints'; @use './../mixins/utilities'; -// Post margins, paddings and sizes +// Post sizes @each $breakpoint in map.keys(breakpoints.$grid-breakpoints) { @include media-breakpoint-up($breakpoint) { $infix: if($breakpoint == 'xs', '', '-#{$breakpoint}'); @each $prop, $abbrev - in ( - margin: m, - padding: p, - /* Deprecated */ line-height: lh, - height: h, - max-height: mh, - width: w, - max-width: mw, - gap: gap - ) + in (/* Deprecated */ line-height: lh, height: h, max-height: mh, width: w, max-width: mw) { @each $size, $length in spacing.$post-sizes { - @if ($prop == margin or $prop == padding) { - .#{$abbrev}t#{$infix}-#{$size}, - .#{$abbrev}y#{$infix}-#{$size} { - #{$prop}-top: $length !important; - } - - .#{$abbrev}e#{$infix}-#{$size}, - .#{$abbrev}x#{$infix}-#{$size} { - #{$prop}-right: $length !important; - } - - .#{$abbrev}b#{$infix}-#{$size}, - .#{$abbrev}y#{$infix}-#{$size} { - #{$prop}-bottom: $length !important; - } - - .#{$abbrev}s#{$infix}-#{$size}, - .#{$abbrev}x#{$infix}-#{$size} { - #{$prop}-left: $length !important; - } - } - .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; } diff --git a/packages/styles/src/functions/_string.scss b/packages/styles/src/functions/_string.scss new file mode 100644 index 0000000000..1c79f06f99 --- /dev/null +++ b/packages/styles/src/functions/_string.scss @@ -0,0 +1,13 @@ +@use 'sass:string'; + +@function replace($string, $term, $replacement: '') { + $index: string.index($string, $term); + + @if $index { + $before: string.slice($string, 1, $index - 1); + $after: string.slice($string, $index + string.length($term)); + @return $before + $replacement + replace($after, $term, $replacement); + } + + @return $string; +} diff --git a/packages/styles/src/index.scss b/packages/styles/src/index.scss index 7bf28ea879..fd78bce402 100644 --- a/packages/styles/src/index.scss +++ b/packages/styles/src/index.scss @@ -1,5 +1,6 @@ @forward './variables/options'; +@use './utilities'; @use './elements'; @use './components'; diff --git a/packages/styles/src/intranet.scss b/packages/styles/src/intranet.scss index 15e5ec70d3..5738064522 100644 --- a/packages/styles/src/intranet.scss +++ b/packages/styles/src/intranet.scss @@ -1,5 +1,6 @@ @forward './variables/options'; -@use './elements'; +@use 'utilities'; +@use 'elements'; @use 'components'; @use 'components/intranet-header'; diff --git a/packages/styles/src/post-external.scss b/packages/styles/src/post-external.scss index cc042736d1..87f2a3fa55 100644 --- a/packages/styles/src/post-external.scss +++ b/packages/styles/src/post-external.scss @@ -1,3 +1,4 @@ @use './post-tokens-external'; +@use './utilities'; @use './elements'; @use './components'; diff --git a/packages/styles/src/post-internal.scss b/packages/styles/src/post-internal.scss index 41a9ca2447..834c79f621 100644 --- a/packages/styles/src/post-internal.scss +++ b/packages/styles/src/post-internal.scss @@ -1,3 +1,4 @@ @use './post-tokens-internal'; +@use './utilities'; @use './elements'; @use './components'; diff --git a/packages/styles/src/themes/bootstrap/_utilities.scss b/packages/styles/src/themes/bootstrap/_utilities.scss index cff122af29..01cb191d0c 100644 --- a/packages/styles/src/themes/bootstrap/_utilities.scss +++ b/packages/styles/src/themes/bootstrap/_utilities.scss @@ -6,4 +6,24 @@ // manipulate $utilities before you import utilities/api $utilities: map.remove($utilities, 'background-color'); +$utilities: map.remove($utilities, 'margin'); +$utilities: map.remove($utilities, 'margin-x'); +$utilities: map.remove($utilities, 'margin-y'); +$utilities: map.remove($utilities, 'margin-top'); +$utilities: map.remove($utilities, 'margin-end'); +$utilities: map.remove($utilities, 'margin-bottom'); +$utilities: map.remove($utilities, 'margin-start'); + +$utilities: map.remove($utilities, 'padding'); +$utilities: map.remove($utilities, 'padding-x'); +$utilities: map.remove($utilities, 'padding-y'); +$utilities: map.remove($utilities, 'padding-top'); +$utilities: map.remove($utilities, 'padding-end'); +$utilities: map.remove($utilities, 'padding-bottom'); +$utilities: map.remove($utilities, 'padding-start'); + +$utilities: map.remove($utilities, 'gap'); +$utilities: map.remove($utilities, 'row-gap'); +$utilities: map.remove($utilities, 'column-gap'); + @import 'bootstrap/scss/utilities/api'; diff --git a/packages/styles/src/utilities/_mixins.scss b/packages/styles/src/utilities/_mixins.scss new file mode 100644 index 0000000000..01e84ab529 --- /dev/null +++ b/packages/styles/src/utilities/_mixins.scss @@ -0,0 +1,23 @@ +@use '../mixins/media'; +@use '../variables/breakpoints'; + +@mixin generate-utilities($properties, $value, $prefix, $suffix, $infix: '') { + .#{$prefix}#{$infix}#{$suffix} { + @each $property in $properties { + #{$property}: #{$value}; + } + } +} + +@mixin generate-responsive-utilities($properties, $value, $prefix, $suffix) { + @each $breakpoint, $min-width in breakpoints.$grid-breakpoints { + @if ($min-width == 0) { + @include generate-utilities($properties, $value, $prefix, $suffix); + } @else { + @include media.min($min-width) { + $infix: '-#{$breakpoint}'; + @include generate-utilities($properties, $value, $prefix, $suffix, $infix); + } + } + } +} diff --git a/packages/styles/src/utilities/_variables.scss b/packages/styles/src/utilities/_variables.scss new file mode 100644 index 0000000000..d7c1bca0e7 --- /dev/null +++ b/packages/styles/src/utilities/_variables.scss @@ -0,0 +1,86 @@ +@use '../tokens/utilities' as tokens; + +/* + Add new utilities using the following structure: + [set]: ( + tokens: map (required), + classes: ( + [group]: ( + responsive: boolean (optional), + prefixes: map (required), + ) + ) + ) + + - `set`: + The name of the token set (e.g., if the tokens are contained in the "$post-spacing" map, the set is "spacing"). + + - `tokens`: + The map of tokens that should be used to generate the utility classes. + + - `group`: + The group name used in the token keys (e.g., if the tokens are named "post-utility-margin-*", the group is "margin"). + + - `responsive`: + If set to `true`, the utility classes will be generated for all breakpoints (e.g., `-sm`, `-md`, `-lg`, etc.). + If set to `false` or omitted, utilities will be generated without a breakpoint infix. + + - `prefixes`: + A map where each key is the class name prefix and the value is the CSS property (or properties) that the class will set. + + Example: + spacing: ( + tokens: tokens.$post-spacing, // Refers to the token map containing all spacing values + classes: ( + margin: ( // Refers to the token "post-utility-margin-*" token in above map + classes: ( + m: margin, // Generates `.m-*` classes to set the `margin` property + mx: margin-left margin-right, // Generates `.mx-*` classes to set `margin-left` and `margin-right` properties + ... + ), + responsive: true, // Generates responsive classes + ) + ) + ) +*/ + +$utilities: ( + spacing: ( + tokens: tokens.$post-spacing, + classes: ( + margin: ( + responsive: true, + prefixes: ( + m: margin, + mx: margin-inline, + ms: margin-inline-start, + me: margin-inline-end, + my: margin-block, + mt: margin-block-start, + mb: margin-block-end, + ), + ), + padding: ( + responsive: true, + prefixes: ( + p: padding, + px: padding-inline, + ps: padding-inline-start, + pe: padding-inline-end, + py: padding-block, + pt: padding-block-start, + pb: padding-block-end, + ), + ), + gap: ( + responsive: true, + prefixes: ( + gap: gap, + row-gap: row-gap, + column-gap: column-gap, + ), + ), + ), + ), + // IMPORTANT: When adding new utilities here, please ensure to remove the corresponding bootstrap utilities in `src/themes/bootstrap/_utilities.scss`. +); diff --git a/packages/styles/src/utilities/index.scss b/packages/styles/src/utilities/index.scss new file mode 100644 index 0000000000..4faeb72b77 --- /dev/null +++ b/packages/styles/src/utilities/index.scss @@ -0,0 +1,30 @@ +@use 'sass:map'; + +@use '../functions/string'; + +@use './mixins' as *; +@use './variables' as *; + +@use './temp/legacy'; + +@each $set, $config in $utilities { + $tokens: map.get($config, tokens); + $classes: map.get($config, classes); + + @each $group, $classesConfig in $classes { + $responsive: map.get($classesConfig, responsive); + $prefixes: map.get($classesConfig, prefixes); + + @each $key, $value in $tokens { + $suffix: string.replace($key, 'post-utility-#{$group}'); + + @each $prefix, $properties in $prefixes { + @if $responsive { + @include generate-responsive-utilities($properties, $value, $prefix, $suffix); + } @else { + @include generate-utilities($properties, $value, $prefix, $suffix); + } + } + } + } +} diff --git a/packages/styles/src/components/utilities.scss b/packages/styles/src/utilities/temp/_legacy.scss similarity index 79% rename from packages/styles/src/components/utilities.scss rename to packages/styles/src/utilities/temp/_legacy.scss index 4ab4e9c196..3a963fc06c 100644 --- a/packages/styles/src/components/utilities.scss +++ b/packages/styles/src/utilities/temp/_legacy.scss @@ -1,16 +1,14 @@ -@forward './../variables/options'; - @use 'sass:map'; @use 'sass:color' as sass-color; @use 'sass:list'; -@use './../themes/bootstrap/core' as *; -@use './../themes/bootstrap/utilities' as bu; // TODO: Move Design System utilities to utilities folder. -@use './../mixins/color' as color-mx; -@use './../mixins/utilities'; -@use './../variables/color'; -@use './../variables/spacing'; -@use './../variables/grid'; +@use '../../themes/bootstrap/core' as *; +@use '../../themes/bootstrap/utilities' as bu; // TODO: Move Design System utilities to utilities folder. +@use '../../mixins/color' as color-mx; +@use '../../mixins/utilities'; +@use '../../variables/color'; +@use '../../variables/spacing'; +@use '../../variables/grid'; .bold { font-weight: 700; diff --git a/packages/styles/tests/components/utilities.test.scss b/packages/styles/tests/components/utilities.test.scss deleted file mode 100644 index c6f8ac82dd..0000000000 --- a/packages/styles/tests/components/utilities.test.scss +++ /dev/null @@ -1,7 +0,0 @@ -@use 'sass:map'; -@use 'sass:meta'; -@use 'tests/jest'; -@use 'src/components/utilities'; - -// Check if component forwards options -@include jest.true(map.has-key(meta.module-variables('utilities'), 'font-base-path')); diff --git a/packages/styles/tests/functions/string.test.scss b/packages/styles/tests/functions/string.test.scss new file mode 100644 index 0000000000..2247cf29a6 --- /dev/null +++ b/packages/styles/tests/functions/string.test.scss @@ -0,0 +1,22 @@ +@use 'tests/jest'; +@use 'src/functions/string'; + +$paragraph: "I think Ruth's dog is cuter than your dog!"; + +// it should replace a term by another +@include jest.equal( + 'I think my dog is cuter than your dog!', + string.replace($paragraph, "Ruth's", 'my') +); + +// it should replace all occurrences +@include jest.equal( + "I think Ruth's cat is cuter than your cat!", + string.replace($paragraph, "dog", 'cat') +); + +// it should return the original string when it does not contain the term to replace +@include jest.equal( + $paragraph, + string.replace($paragraph, "Sandy's", 'my') +); diff --git a/packages/styles/tests/utilities/index.test.scss b/packages/styles/tests/utilities/index.test.scss new file mode 100644 index 0000000000..65b6554c4f --- /dev/null +++ b/packages/styles/tests/utilities/index.test.scss @@ -0,0 +1,16 @@ +@use 'sass:map'; +@use 'sass:meta'; +@use 'tests/jest'; +@use 'src/utilities'; + + +.test { + // it should include legacy utilities + @extend .bold; + + // it should correctly create utilities from tokens + @extend .m-16; + @extend .mt-24; + @extend .m-lg-8; + @extend .my-sm-48; +} diff --git a/packages/styles/tests/utilities/mixins.test.scss b/packages/styles/tests/utilities/mixins.test.scss new file mode 100644 index 0000000000..8792c4c959 --- /dev/null +++ b/packages/styles/tests/utilities/mixins.test.scss @@ -0,0 +1,11 @@ +@use 'sass:map'; +@use 'sass:meta'; +@use 'tests/jest'; +@use 'src/utilities/mixins'; + + +.test { + @include mixins.generate-utilities('font-weight', '400', 'fw', 'normal'); + @include mixins.generate-utilities('font-size', '1.5rem', 'fs', 'large', 'sm'); + @include mixins.generate-responsive-utilities('row-gap', '48px', 'rg', '48'); +}