From 9ee1534432cc156c90c28066b668f77584d2bf54 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 12 Jan 2021 16:46:13 -0600 Subject: [PATCH] feat(layout): add support for sass modules --- packages/layout/.gitignore | 2 +- packages/layout/index.scss | 12 + packages/layout/package.json | 2 +- packages/layout/scss/modules/_breakpoint.scss | 232 ++++++++++++++++++ packages/layout/scss/modules/_convert.scss | 40 +++ packages/layout/scss/modules/_spacing.scss | 9 + packages/layout/scss/modules/_utilities.scss | 41 ++++ packages/layout/tasks/build.js | 80 ++++++ 8 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 packages/layout/index.scss create mode 100644 packages/layout/scss/modules/_breakpoint.scss create mode 100644 packages/layout/scss/modules/_convert.scss create mode 100644 packages/layout/scss/modules/_spacing.scss create mode 100644 packages/layout/scss/modules/_utilities.scss diff --git a/packages/layout/.gitignore b/packages/layout/.gitignore index e1ceefc21865..86d4c2dd380e 100644 --- a/packages/layout/.gitignore +++ b/packages/layout/.gitignore @@ -1 +1 @@ -scss/generated +generated diff --git a/packages/layout/index.scss b/packages/layout/index.scss new file mode 100644 index 000000000000..aba783cac8b9 --- /dev/null +++ b/packages/layout/index.scss @@ -0,0 +1,12 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward 'scss/modules/breakpoint'; +@forward 'scss/modules/convert'; +@forward 'scss/modules/spacing'; +// TODO: should these be public? +@forward 'scss/modules/utilities'; diff --git a/packages/layout/package.json b/packages/layout/package.json index 865ef93669ef..eba6d08aca1c 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -26,7 +26,7 @@ }, "scripts": { "build": "yarn clean && bundler bundle src/index.js --name CarbonLayout && node tasks/build.js && bundler sassdoc \"scss/**/*.scss\"", - "clean": "rimraf es lib umd scss/generated" + "clean": "rimraf es lib umd scss/generated scss/modules/generated" }, "devDependencies": { "@carbon/bundler": "^10.11.0", diff --git a/packages/layout/scss/modules/_breakpoint.scss b/packages/layout/scss/modules/_breakpoint.scss new file mode 100644 index 000000000000..e1dbcc9681e6 --- /dev/null +++ b/packages/layout/scss/modules/_breakpoint.scss @@ -0,0 +1,232 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +// https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_breakpoints.scss +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:meta'; +@use 'convert'; +@use 'utilities'; + +/// Carbon gutter size in rem +/// @type Number +/// @access public +/// @group @carbon/layout +$grid-gutter: convert.rem(32px); + +/// Carbon condensed gutter size in rem +/// @type Number +/// @access public +/// @group @carbon/layout +$grid-gutter--condensed: convert.rem(1px); + +// Initial map of our breakpoints and their values +/// @type Map +/// @access public +/// @group @carbon/layout +$grid-breakpoints: ( + sm: ( + columns: 4, + margin: 0, + width: convert.rem(320px), + ), + md: ( + columns: 8, + margin: convert.rem(16px), + width: convert.rem(672px), + ), + lg: ( + columns: 16, + margin: convert.rem(16px), + width: convert.rem(1056px), + ), + xlg: ( + columns: 16, + margin: convert.rem(16px), + width: convert.rem(1312px), + ), + max: ( + columns: 16, + margin: convert.rem(24px), + width: convert.rem(1584px), + ), +) !default; + +/// Get the value of the next breakpoint, or null for the last breakpoint +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @param {List} $breakpoint-names [map-keys($breakpoints)] - A list of names from the `$breakpoints` map +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-next( + $name, + $breakpoints: $grid-breakpoints, + $breakpoint-names: map.keys($breakpoints) +) { + $n: list.index($breakpoint-names, $name); + @if $n != null and $n < list.length($breakpoint-names) { + @return list.nth($breakpoint-names, $n + 1); + } + @return null; +} + +/// Get the value of the previous breakpoint, or null for the first breakpoint +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @param {List} $breakpoint-names [map-keys($breakpoints)] - A list of names from the `$breakpoints` map +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-prev( + $name, + $breakpoints: $grid-breakpoints, + $breakpoint-names: map.keys($breakpoints) +) { + $n: list.index($breakpoint-names, $name); + @if $n != null and $n > 1 { + @return list.nth($breakpoint-names, $n - 1); + } + @return null; +} + +/// Check to see if the given breakpoint name +/// @param {String} $name - The name of the brekapoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name of the breakpoint and the value is the values for the breakpoint +/// @return {Bool} +/// @access public +/// @group @carbon/layout +@function is-smallest-breakpoint($name, $breakpoints: $grid-breakpoints) { + @return list.index(map.keys($breakpoints), $name) == 1; +} + +/// Returns the largest breakpoint name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @return {String} +/// @access public +/// @group @carbon/layout +@function largest-breakpoint-name($breakpoints: $grid-breakpoints) { + $total-breakpoints: list.length($breakpoints); + @return key-by-index($breakpoints, $total-breakpoints); +} + +/// Get the infix for a given breakpoint in a list of breakpoints. Usesful for generate the size part in a selector, for example: `.prefix--col-sm-2`. +/// @param {String} $name - The name of the breakpoint +/// @return {String} +/// @access public +/// @group @carbon/layout +@function breakpoint-infix($name) { + @return '-#{$name}'; +} + +/// Generate a media query from the width of the given breakpoint to infinity +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-up($name, $breakpoints: $grid-breakpoints) { + @if meta.type-of($name) == 'number' { + @media (min-width: $name) { + @content; + } + } @else if map.has-key($breakpoints, $name) { + $breakpoint: map.get($breakpoints, $name); + $width: map.get($breakpoint, width); + @if is-smallest-breakpoint($name, $breakpoints) { + @content; + } @else { + @media (min-width: $width) { + @content; + } + } + } @else { + @error 'Unable to find a breakpoint with name `#{$name}`. Expected one of: (#{map.keys($breakpoints)})'; + } +} + +/// Generate a media query for the maximum width of the given styles +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-down($name, $breakpoints: $grid-breakpoints) { + @if meta.type-of($name) == 'number' { + @media (max-width: $name) { + @content; + } + } @else if map.has-key($breakpoints, $name) { + // We borrow this logic from bootstrap for specifying the value of the + // max-width. The maximum width is calculated by finding the breakpoint and + // subtracting .02 from its value. This value is used instead of .01 to + // avoid rounding issues in Safari + // https://github.com/twbs/bootstrap/blob/c5b1919deaf5393fcca9e9b9d7ce9c338160d99d/scss/mixins/_breakpoints.scss#L34-L46 + $breakpoint: map.get($breakpoints, $name); + $width: map.get($breakpoint, width) - 0.02; + @media (max-width: $width) { + @content; + } + } @else { + @error 'Unable to find a breakpoint with name `#{$name}`. Expected one of: (#{map.keys($breakpoints)})'; + } +} + +/// Generate a media query for the range between the lower and upper breakpoints +/// @param {String | Number} $lower +/// @param {String | Number} $upper +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) { + $is-number-lower: meta.type-of($lower) == 'number'; + $is-number-upper: meta.type-of($upper) == 'number'; + $min: if($is-number-lower, $lower, map.get($breakpoints, $lower)); + $max: if($is-number-upper, $upper, map.get($breakpoints, $upper)); + + @if $min and $max { + $min-width: if(not $is-number-lower and $min, map.get($min, width), $min); + $max-width: if(not $is-number-upper and $max, map.get($max, width), $max); + @media (min-width: $min-width) and (max-width: $max-width) { + @content; + } + } @else if $min != null and $max == null { + @include breakpoint-up($lower) { + @content; + } + } @else if $min == null and $max != null { + @include breakpoint-down($upper) { + @content; + } + } @else { + @error 'Unable to find a breakpoint to satisfy: (#{$lower},#{$upper}). Expected both to be one of (#{map.keys($breakpoints)}).'; + } +} + +/// Generate media query for the largest breakpoint +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin largest-breakpoint($breakpoints: $grid-breakpoints) { + @include breakpoint(largest-breakpoint-name()) { + @content; + } +} + +/// Generate a media query for a given breakpoint +/// @param {String | Number} $name +/// @param {Map} $breakpoints [$grid-breakpoints] - A map of breakpoints where the key is the name +/// @content +/// @access public +/// @group @carbon/layout +@mixin breakpoint($name, $breakpoints: $grid-breakpoints) { + @include breakpoint-up($name, $breakpoints) { + @content; + } +} diff --git a/packages/layout/scss/modules/_convert.scss b/packages/layout/scss/modules/_convert.scss new file mode 100644 index 000000000000..a7ec602db518 --- /dev/null +++ b/packages/layout/scss/modules/_convert.scss @@ -0,0 +1,40 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +/// Default font size +/// @type Number +/// @access public +/// @group @carbon/layout +$base-font-size: 16px !default; + +/// Convert a given px unit to a rem unit +/// @param {Number} $px - Number with px unit +/// @return {Number} Number with rem unit +/// @access public +/// @group @carbon/layout +@function rem($px) { + @if unit($px) != 'px' { + // TODO: update to @error in v11 + @warn "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`"; + } + + @return ($px / $base-font-size) * 1rem; +} + +/// Convert a given px unit to a em unit +/// @param {Number} $px - Number with px unit +/// @return {Number} Number with em unit +/// @access public +/// @group @carbon/layout +@function em($px) { + @if unit($px) != 'px' { + // TODO: update to @error in v11 + @warn "Expected argument $px to be of type `px`, instead received: `#{unit($px)}`"; + } + + @return ($px / $base-font-size) * 1em; +} diff --git a/packages/layout/scss/modules/_spacing.scss b/packages/layout/scss/modules/_spacing.scss new file mode 100644 index 000000000000..42e124108c91 --- /dev/null +++ b/packages/layout/scss/modules/_spacing.scss @@ -0,0 +1,9 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +@forward './generated/fluid-spacing'; +@forward './generated/spacing'; diff --git a/packages/layout/scss/modules/_utilities.scss b/packages/layout/scss/modules/_utilities.scss new file mode 100644 index 000000000000..26a423de7b48 --- /dev/null +++ b/packages/layout/scss/modules/_utilities.scss @@ -0,0 +1,41 @@ +// +// Copyright IBM Corp. 2018, 2018 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +/// Map deep get +/// @author Hugo Giraudel +/// @access public +/// @param {Map} $map - Map +/// @param {Arglist} $keys - Key chain +/// @return {*} Desired value +/// @group @carbon/layout +@function map-deep-get($map, $keys...) { + @each $key in $keys { + $map: map-get($map, $key); + } + @return $map; +} + +/// Provide a map and index, and get back the relevant key value +/// @access public +/// @param {Map} $map - Map +/// @param {Integer} $index - Key chain +/// @return {String} Desired value +/// @group @carbon/layout +@function key-by-index($map, $index) { + $keys: map-keys($map); + @return nth($keys, $index); +} + +/// Pass in a map, and get the last one in the list back +/// @access public +/// @param {Map} $map - Map +/// @return {*} Desired value +/// @group @carbon/layout +@function last-map-item($map) { + $total-length: length($map); + @return map-get($map, carbon--key-by-index($map, $total-length)); +} diff --git a/packages/layout/tasks/build.js b/packages/layout/tasks/build.js index f09adee7d432..188cabbca73f 100644 --- a/packages/layout/tasks/build.js +++ b/packages/layout/tasks/build.js @@ -27,7 +27,9 @@ async function build() { reporter.info('Building scss files for layout...'); const SCSS_DIR = path.resolve(__dirname, '../scss/generated'); + const MODULES_DIR = path.resolve(__dirname, '../scss/modules/generated'); const files = [ + // v10 { filepath: path.join(SCSS_DIR, '_spacing.scss'), builder() { @@ -58,9 +60,24 @@ async function build() { return buildTokenFile(iconSize, 'icon-size'); }, }, + + // v11 + { + filepath: path.join(MODULES_DIR, '_spacing.scss'), + builder() { + return buildModulesTokenFile(spacing, 'spacing', ''); + }, + }, + { + filepath: path.join(MODULES_DIR, '_fluid-spacing.scss'), + builder() { + return buildModulesTokenFile(fluidSpacing, 'fluid-spacing', ''); + }, + }, ]; await fs.ensureDir(SCSS_DIR); + await fs.ensureDir(MODULES_DIR); for (const { filepath, builder } of files) { const { code } = generate(builder()); await fs.writeFile(filepath, code); @@ -149,6 +166,69 @@ function buildTokenFile(tokenScale, group) { ...aliases, ]); } +/** + * Build a Sass Module token stylesheet for a given token scale and group. This will help + * generate the initial collection of tokens and a list of all tokens. In + * addition, it will generate aliases for these tokens. + * + * @param {Array} tokenScale + * @param {string} group + * @returns {StyleSheet} + */ +function buildModulesTokenFile(tokenScale, group) { + const FILE_BANNER = t.Comment(` Code generated by @carbon/layout. DO NOT EDIT. + + Copyright IBM Corp. 2018, 2019 + + This source code is licensed under the Apache-2.0 license found in the + LICENSE file in the root directory of this source tree. +`); + + const values = tokenScale.map((value, index) => { + const name = formatStep(`${group}`, index + 1); + const shorthand = formatStep(group, index + 1); + const id = t.Identifier(name); + return [ + name, + shorthand, + id, + t.Assignment({ + id, + init: t.SassValue(value), + default: true, + }), + ]; + }); + + const variables = values.flatMap(([_name, _shorthand, _id, assignment]) => { + const comment = t.Comment(`/ @type Number +/ @access public +/ @group @carbon/layout`); + return [comment, assignment, t.Newline()]; + }); + + const list = [ + t.Comment(`/ @type List +/ @access public +/ @group @carbon/layout`), + t.Assignment({ + id: t.Identifier(group), + init: t.SassList({ + elements: values.map(([_name, _shorthand, id]) => { + return id; + }), + }), + }), + ]; + + return t.StyleSheet([ + FILE_BANNER, + t.Newline(), + ...variables, + ...list, + t.Newline(), + ]); +} /** * Format the given step for a token name. Most often, this is to pad a `0` for