diff --git a/config/stylelint/sassMsgFormatter.js b/config/stylelint/sassMsgFormatter.js new file mode 100644 index 0000000000..2c42f00406 --- /dev/null +++ b/config/stylelint/sassMsgFormatter.js @@ -0,0 +1,123 @@ +const chalk = require('chalk'); + +/** + * @constant + */ +const ERROR = chalk.bold.red; +const WARNING = chalk.yellow; +const URL = chalk.underline.cyan; +const NUMBER = chalk.dim; +const RULE = chalk.bgWhite.black; + +/** + * returns the boolean representing whether a file has any errors for filtering purposes + * @param {Object} result - the object representing the result of a linting session + * @returns {Boolean} returns true or false depending on whether the file has errors or not + */ +function filterForErrors(result) { + return result.errored; +} + +/** + * creates colored text that describes the severity of a problem + * @param {String} severity - error.severity, the severity of the error + * @returns {String} returns colored error text depending on the severity + */ +function generateErrorIcon(severity) { + let errorIcon = ''; + switch (severity) { + case 'error': + errorIcon = ERROR('ERROR'); + break; + case 'warning': + errorIcon = WARNING('warning'); + break; + default: + errorIcon += ''; + break; + } + return errorIcon; +} + +/** + * indents a piece of text depending on the length of the previous block of text + * @param {Object} error - the specific error + * @returns {String} returns the appropriate amount of tab characters + */ +function formatTabbing(error) { + if (`${error.line}:${error.column}`.length < 6) { + return '\t \t'; + } + return '\t'; +} + +/** + * creates a message with a link to the carbon documentation depending on the individual error + * @param {String} text - the error.text property of an error message, the defaul error message stylelint produces + * @returns {String} returns a custom informational message for the type of error + */ +function createCustomMessage(text) { + let message = ''; + if (text) { + if (text.includes('color"')) { + const url = URL('https://www.carbondesignsystem.com/guidelines/color/usage'); + message = `\n\t${text}\n\t> Please refer to the Carbon documentation for proper color tokens: ${url}`; + } else if (text.includes('"margin') || text.includes('"padding')) { + const url = URL('https://www.carbondesignsystem.com/guidelines/spacing#spacing-scale'); + message = `\n\t${text}\n\t> Please refer to the Carbon documentation for proper spacing values: ${url}`; + } else if (text.includes('"font')) { + const url = URL('https://www.carbondesignsystem.com/guidelines/typography/productive'); + message = `\n\t${text}\n\t> Please refer to the Carbon productive typography documentation for proper font values: ${url}`; + } else if (text.includes('transition')) { + const url = URL('https://www.carbondesignsystem.com/guidelines/motion/overview'); + message = `\n\t${text}\n\t> Please refer to the Carbon motion documentation for transitions: ${url}`; + } + } + return message; +} + +/** + * formats the error message + * @param {Array} errors - an array of all the errors in the linting session + * @returns {String} returns a formatted error message + */ + +function formatError(errors) { + let errorMsg = ''; + errors.forEach((error, i) => { + const number = NUMBER(`${i + 1}.`); + errorMsg += `${number} \t${generateErrorIcon(error.severity)} \t ${error.line}:${ + error.column + } ${formatTabbing(error)} ${RULE(error.rule)}\n \t${createCustomMessage(error.text)}\n\n`; + }); + + return errorMsg; +} + +/** + * @type {import('stylelint').Formatter} + */ +function formatter(results) { + let formattedMsg = ''; + if (results) { + const filesWithErrors = results.filter(filterForErrors); + if (filesWithErrors.length > 0) { + formattedMsg += TITLE('\n!! WARNINGS !!\n\n'); + } + filesWithErrors.forEach(result => { + const errors = result.warnings; + const errorMessage = formatError(errors); + formattedMsg += chalk.bold('Source: '); + formattedMsg += `${result.source}\n`; + formattedMsg += `${errorMessage}\n`; + }); + } + return formattedMsg; +} + +const formatterModule = (module.exports = formatter); +formatterModule.filterForErrors = filterForErrors; +formatterModule.generateErrorIcon = generateErrorIcon; +formatterModule.formatTabbing = formatTabbing; +formatterModule.createCustomMessage = createCustomMessage; +formatterModule.formatError = formatError; diff --git a/config/stylelint/sassMsgFormatter.test.js b/config/stylelint/sassMsgFormatter.test.js new file mode 100644 index 0000000000..b8b7076105 --- /dev/null +++ b/config/stylelint/sassMsgFormatter.test.js @@ -0,0 +1,89 @@ +const formatter = require('./sassMsgFormatter'); +const chalk = require('chalk'); + +const ERROR = chalk.bold.red; +const WARNING = chalk.yellow; +const URL = chalk.underline.cyan; +const TITLE = chalk.bgYellow; + +const exampleOne = { + source: 'test1.js', + errored: true, + warnings: [ + { + severity: 'error', + line: 21, + column: 33, + rule: 'declaration-property-unit-blacklist', + text: 'Unexpected value in property "color"', + }, + ], +}; +const exampleTwo = { + source: 'test2.js', + errored: true, + warnings: [ + { + severity: 'error', + line: 123, + column: 76, + rule: 'declaration-property-value-blacklist', + text: 'Unexpected value in "font-family"', + }, + ], +}; + +describe('sassMsgFormatter', () => { + it('filters for errors', () => { + // this function should return false unless errored is equal to true + expect(formatter.filterForErrors({ errored: undefined })).toBeFalsy(); + expect(formatter.filterForErrors({ errored: true })).toBeTruthy(); + expect(formatter.filterForErrors({ errored: false })).toBeFalsy(); + expect(formatter.filterForErrors({ errored: null })).toBeFalsy(); + }); + it('generates error icons', () => { + // expect(generateErrorIcon('error')).toEqual(ERROR('ERROR')); + expect(formatter.generateErrorIcon('error')).toContain('ERROR'); + expect(formatter.generateErrorIcon('warning')).toContain('warning'); + expect(formatter.generateErrorIcon(undefined)).toHaveLength(0); + }); + it('formats tabbing', () => { + expect(formatter.formatTabbing({ line: 121, column: 23 })).toHaveLength(1); + expect(formatter.formatTabbing({ line: 1, column: 123 })).toHaveLength(3); + expect(formatter.formatTabbing({ line: 14, column: 23 })).toHaveLength(3); + }); + it('creates custom message', () => { + console.log(exampleOne.warnings[0].text); + expect(createCustomMessage(exampleOne.warnings[0].text)).toContain( + URL('https://www.carbondesignsystem.com/guidelines/color/usage') + ); + expect(createCustomMessage(exampleTwo.warnings[0].text)).toContain( + URL('https://www.carbondesignsystem.com/guidelines/typography/productive') + ); + expect(createCustomMessage(null)).toHaveLength(0); + }); + it('formats errors', () => { + expect(formatter.formatError(exampleOne.warnings)).toContain(ERROR('ERROR')); + expect(formatter.formatError(exampleTwo.warnings)).toContain(ERROR('ERROR')); + expect( + formatError([ + { + severity: 'warning', + line: 123, + column: 45, + rule: 'insert-rule-here', + text: 'error text', + }, + ]) + ).toContain(WARNING('warning')); + }); + it('formats message', () => { + const resultsTest = [exampleOne, exampleTwo, { errored: false, warnings: undefined }]; + expect(resultsTest.filter(formatter.filterForErrors)).toHaveLength(2); + expect(formatter(resultsTest)).toContain(TITLE('\n!! WARNINGS !!\n\n')); + expect(formatter(resultsTest)).toContain(formatError(exampleOne.warnings)); + expect(formatter(resultsTest)).toContain(formatError(exampleTwo.warnings)); + expect(formatter([{ errored: false, warnings: undefined }])).toMatch(''); + expect(formatter(null)).toHaveLength(0); + }); +}); diff --git a/package.json b/package.json index 3fa98a7362..77a33ed8de 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "format:diff": "prettier --list-different \"**/*.{scss,css,js,jsx,md,ts}\"", "lint": "yarn lint:javascript && yarn lint:stylelint", "lint:javascript": "eslint --ext .jsx --ext .js .", - "lint:stylelint": "stylelint './src/**/*.scss' --syntax scss --ignorePath .gitignore", + "lint:stylelint": "stylelint './src/**/*.scss' --syntax scss --ignorePath .gitignore --custom-formatter ./config/stylelint/sassMsgFormatter.js", + "prepare": "yarn build", "publish-npm": "yarn semantic-release", "start": "yarn test:engines && yarn storybook", "storybook": "yarn test:engines && start-storybook -p 3000 -s public/development", @@ -47,7 +48,44 @@ "extends": "stylelint-config-recommended-scss", "plugins": [ "stylelint-scss" - ] + ], + "rules": { + "declaration-property-unit-blacklist": [ + { + "font-size": [ + "em", + "px", + "pt" + ], + "margin": [ + "px", + "rem" + ], + "padding": [ + "px", + "rem" + ], + "transition": [ + "s", + "ms" + ] + }, + { + "severity": "error" + } + ], + "declaration-property-value-blacklist": [ + { + "color": [ + "/^#/", + "/^rgb/" + ] + }, + { + "severity": "error" + } + ] + } }, "husky": { "hooks": { @@ -162,6 +200,7 @@ "babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-styled-components": "^1.10.7", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "chalk": "^4.1.0", "check-node-version": "^4.0.3", "coveralls": "^3.0.2", "cross-env": "^6.0.3", diff --git a/src/components/GaugeCard/_gauge-card.scss b/src/components/GaugeCard/_gauge-card.scss index f982866bfc..ae9493d686 100644 --- a/src/components/GaugeCard/_gauge-card.scss +++ b/src/components/GaugeCard/_gauge-card.scss @@ -32,7 +32,7 @@ stroke-dasharray: 0 var(--stroke-dash-array); transform: rotate(-90deg); transform-origin: center; - transition: all 0.2s ease-in; + transition: all $duration--moderate-01 ease-in; .#{$iot-prefix}--gauge__loaded & { stroke-dasharray: var(--stroke-dash) var(--stroke-dash-array); diff --git a/src/components/Header/_header.scss b/src/components/Header/_header.scss index 483b6f4d0a..81d1d33a20 100644 --- a/src/components/Header/_header.scss +++ b/src/components/Header/_header.scss @@ -59,7 +59,7 @@ .#{$prefix}--header__menu .#{$prefix}--header__menu-item[role='menuitem'] { display: flex; align-items: center; - color: #c6c6c6; + color: $active-ui; height: 100%; font-size: 0.875rem; font-weight: 400; @@ -68,7 +68,8 @@ text-decoration: none; user-select: none; border: 2px solid #0000; - transition: background-color 110ms, border-color 110ms, color 110ms; + transition: background-color $duration--fast-02, border-color $duration--fast-02, + color $duration--fast-02; justify-content: center; padding: 0; width: 100%; @@ -100,10 +101,7 @@ // Used for links that are directly in the menubar to span the full height height: 100%; // Text styles - font-size: rem(14px); - font-weight: 400; - letter-spacing: 0; - line-height: rem(18px); + @include carbon--type-style('body-short-01'); // Reset link styles and make sure the text isn't selectable text-decoration: none; user-select: none; @@ -119,7 +117,7 @@ button.#{$prefix}--header__menu-item { &:focus { border-color: #fff; - color: #f4f4f4; + color: $ui-02; outline: none; } } diff --git a/src/components/SideNav/SideNav.story.scss b/src/components/SideNav/SideNav.story.scss index 8a8d098085..d843d7ce53 100644 --- a/src/components/SideNav/SideNav.story.scss +++ b/src/components/SideNav/SideNav.story.scss @@ -3,7 +3,7 @@ .#{$iot-prefix}--main-content { padding-top: 3rem; height: 100%; - transition: all 0.11s cubic-bezier(0.2, 0, 1, 0.9); + transition: all $duration--fast-02 cubic-bezier(0.2, 0, 1, 0.9); width: 100%; @media (min-width: 66em) { diff --git a/src/components/SideNav/_side-nav.scss b/src/components/SideNav/_side-nav.scss index 987f38857d..276c9ee164 100644 --- a/src/components/SideNav/_side-nav.scss +++ b/src/components/SideNav/_side-nav.scss @@ -43,7 +43,8 @@ padding: 0 $spacing-05; position: relative; text-decoration: none; - transition: color 110ms, background-color 110ms, outline 110ms; + transition: color $duration--fast-02, background-color $duration--fast-02, + outline $duration--fast-02; &:focus { outline: 2px solid $interactive-01; diff --git a/src/components/TableCard/_table-card.scss b/src/components/TableCard/_table-card.scss index 155b9d5b1d..c619000e67 100644 --- a/src/components/TableCard/_table-card.scss +++ b/src/components/TableCard/_table-card.scss @@ -27,7 +27,7 @@ p { margin-bottom: 8px; - font-size: 14px; + font-size: carbon--type-scale(2); font-weight: 600; } } diff --git a/src/components/TileCatalog/_catalog-content.scss b/src/components/TileCatalog/_catalog-content.scss index 6f87b2c24f..04d68db0fb 100644 --- a/src/components/TileCatalog/_catalog-content.scss +++ b/src/components/TileCatalog/_catalog-content.scss @@ -22,7 +22,7 @@ } .#{$iot-prefix}--sample-tile-title { - color: #3d70b2; + color: $link-01; text-overflow: ellipsis; white-space: nowrap; overflow-x: hidden; @@ -39,7 +39,7 @@ .#{$iot-prefix}--sample-tile-contents { display: flex; flex-flow: column nowrap; - padding: 0 1rem; + padding: 0 $spacing-05; align-self: flex-start; } diff --git a/src/components/TileCatalog/_tile-group.scss b/src/components/TileCatalog/_tile-group.scss index b373bf7dea..3859571bd1 100644 --- a/src/components/TileCatalog/_tile-group.scss +++ b/src/components/TileCatalog/_tile-group.scss @@ -21,7 +21,7 @@ flex: 1 1 33.33%; display: none; min-height: 0px; - padding: 0px; + padding: 0; border-top: 0px; border-bottom: 0px; @media screen and (min-width: $two-pane) { diff --git a/src/components/TileCatalogNew/_tile-catalog.scss b/src/components/TileCatalogNew/_tile-catalog.scss index 24a217ce92..a5abb848de 100644 --- a/src/components/TileCatalogNew/_tile-catalog.scss +++ b/src/components/TileCatalogNew/_tile-catalog.scss @@ -43,7 +43,7 @@ svg { height: 2.5rem; width: 2.5rem; - padding: 0.75rem; + padding: $spacing-04; } } } diff --git a/src/components/TileGallery/_tile-gallery.scss b/src/components/TileGallery/_tile-gallery.scss index bf6e9f358c..9247553f6b 100644 --- a/src/components/TileGallery/_tile-gallery.scss +++ b/src/components/TileGallery/_tile-gallery.scss @@ -169,7 +169,7 @@ transform: translate(-50%, -50%); width: 100%; height: 100%; - transition: 0.5s ease; + transition: $duration--slow-01 ease; opacity: 0; display: flex; align-items: center; diff --git a/src/components/WizardModal/_wizard-modal.scss b/src/components/WizardModal/_wizard-modal.scss index 19318d9804..37f3de81a4 100644 --- a/src/components/WizardModal/_wizard-modal.scss +++ b/src/components/WizardModal/_wizard-modal.scss @@ -5,10 +5,6 @@ min-width: 410px; } - .#{$prefix}--progress { - /* need to pad some space for the focus outlines*/ - padding: 3px; - } .#{$prefix}--progress-step { max-width: 150px; } diff --git a/yarn.lock b/yarn.lock index 6dd83cfcf1..b952c02cda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6230,6 +6230,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + change-emitter@^0.1.2: version "0.1.6" resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"