From fadafde3e2c4a8f0607ad08624515c907920ce9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20B=C3=BCschlen?= Date: Thu, 29 Feb 2024 17:08:49 +0100 Subject: [PATCH] feat(theme): define custom properties for all font styles and curves (#142) --- postcss.config.cjs | 2 + .../generate-component/templates/[name].css | 4 +- scripts/postcss-leu-font-styles.cjs | 160 ++++++++++++++ src/components/accordion/accordion.css | 4 +- src/components/breadcrumb/breadcrumb.css | 4 +- src/components/button/button.css | 4 +- src/components/checkbox/checkbox-group.css | 4 +- src/components/checkbox/checkbox.css | 2 +- src/components/chip/chip.css | 4 +- src/components/input/input.css | 4 +- src/components/menu/menu-item.css | 6 +- src/components/pagination/pagination.css | 2 +- src/components/popup/popup.css | 4 +- src/components/radio/radio-group.css | 4 +- src/components/radio/radio.css | 2 +- src/components/select/select.css | 4 +- src/components/table/table.css | 4 +- src/styles/custom-properties.css | 8 +- src/styles/font-definitions.json | 202 ++++++++++++++++++ stylelint.config.mjs | 2 + 20 files changed, 400 insertions(+), 30 deletions(-) create mode 100644 scripts/postcss-leu-font-styles.cjs create mode 100644 src/styles/font-definitions.json diff --git a/postcss.config.cjs b/postcss.config.cjs index 549ccc8c..13cbb376 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,10 +1,12 @@ const postcssPresetEnv = require("postcss-preset-env") const atImport = require("postcss-import") +const leuFontStyles = require("./scripts/postcss-leu-font-styles.cjs") module.exports = { stage: 2, plugins: [ atImport(), + leuFontStyles(), postcssPresetEnv({ features: { "custom-media-queries": true, diff --git a/scripts/generate-component/templates/[name].css b/scripts/generate-component/templates/[name].css index d130cbe5..f4e3bc00 100644 --- a/scripts/generate-component/templates/[name].css +++ b/scripts/generate-component/templates/[name].css @@ -4,8 +4,8 @@ } :host { - --[name]-font-regular: var(--leu-font-regular); - --[name]-font-black: var(--leu-font-black); + --[name]-font-regular: var(--leu-font-family-regular); + --[name]-font-black: var(--leu-font-family-black); font-family: var(--[name]-font-regular); } diff --git a/scripts/postcss-leu-font-styles.cjs b/scripts/postcss-leu-font-styles.cjs new file mode 100644 index 00000000..344cf5a9 --- /dev/null +++ b/scripts/postcss-leu-font-styles.cjs @@ -0,0 +1,160 @@ +const path = require("path") +const fs = require("fs/promises") + +/* Plugin logic */ + +function getPixelValue(value) { + return parseInt(value.replace("px", ""), 10) +} + +async function parseFile(file) { + const string = await fs.readFile(file, "utf-8") + + return JSON.parse(string) +} + +function generateCustomPropertyDeclarations({ + identifier, + fontSize, + fontWeight, + lineHeight, + spacing, +}) { + const customPropertyPrefix = `--leu-t-${identifier}-${fontWeight}` + + const varFontSize = `${customPropertyPrefix}-font-size` + const varLineHeight = `${customPropertyPrefix}-line-height` + const varSpacing = `${customPropertyPrefix}-spacing` + const varFont = `${customPropertyPrefix}-font` + const varFontFamily = `--leu-font-family-${fontWeight}` + + return [ + { type: "fontSize", name: varFontSize, value: `${fontSize / 16}rem` }, + { type: "lineHeight", name: varLineHeight, value: `${lineHeight}` }, + { type: "spacing", name: varSpacing, value: `${spacing}rem` }, + { + type: "font", + name: varFont, + value: `var(${varFontSize}) / var(${varLineHeight}) var(${varFontFamily})`, + }, + ] +} + +function curveStepDeclarations(curvePrefix, stepStyle) { + const fontSizeVar = stepStyle.declarations.find( + (s) => s.type === "fontSize" + ).name + const lineHeightVar = stepStyle.declarations.find( + (s) => s.type === "lineHeight" + ).name + const spacingVar = stepStyle.declarations.find( + (s) => s.type === "spacing" + ).name + const fontVar = stepStyle.declarations.find((s) => s.type === "font").name + + return [ + { prop: `${curvePrefix}-font-size`, value: ` var(${fontSizeVar})` }, + { prop: `${curvePrefix}-line-height`, value: ` var(${lineHeightVar})` }, + { prop: `${curvePrefix}-spacing`, value: ` var(${spacingVar})` }, + { prop: `${curvePrefix}-font`, value: ` var(${fontVar})` }, + ] +} + +/** + * + * @param {*} file + * @param {import('postcss')} postcss + * @param {import('postcss').Source} nodeSource + * @returns + */ +async function createLeuFontStyleNodes(file, postcss, nodeSource) { + const { definitions, curves } = await parseFile(file) + + const fontStyleDeclarations = definitions.flatMap((style) => { + const fontSize = getPixelValue(style.fontSize) + const fontWeights = Array.isArray(style.fontWeight) + ? style.fontWeight + : [style.fontWeight] + const spacing = getPixelValue(style.spacing) / 16 + + const identifier = (fontSize / 4) * 10 + + return fontWeights.map((fontWeight) => ({ + name: style.name, + fontWeight, + identifier, + declarations: generateCustomPropertyDeclarations({ + identifier, + fontSize, + fontWeight, + lineHeight: style.lineHeight, + spacing, + }), + })) + }) + + const fontStyleNodes = fontStyleDeclarations.flatMap((style) => + style.declarations.map( + ({ name, value }) => + new postcss.Declaration({ prop: name, value, source: nodeSource }) + ) + ) + + const curveNodes = curves.flatMap((curve) => { + const [_, lastStepName] = curve.steps.at(-1) + const { identifier } = fontStyleDeclarations.find( + (style) => style.name === lastStepName && style.fontWeight === "black" + ) + + const curvePrefix = `--leu-t-curve-${identifier}-black` + + return curve.steps.flatMap((step) => { + const [viewport, styleName] = step + + const stepStyle = fontStyleDeclarations.find( + (s) => s.name === styleName && s.fontWeight === "black" + ) + + const nodes = curveStepDeclarations(curvePrefix, stepStyle).map( + ({ prop, value }) => + new postcss.Declaration({ prop, value, source: nodeSource }) + ) + + return viewport === null + ? nodes + : new postcss.AtRule({ + name: "media", + params: `(${viewport})`, + nodes, + source: nodeSource, + }) + }) + }) + + return [...fontStyleNodes, ...curveNodes] +} + +/* Plugin config */ + +/** + * @type {import('postcss').PluginCreator} + */ +module.exports = () => ({ + postcssPlugin: "leu-font-styles", + AtRule: { + "leu-font-styles": async (atRule, postcss) => { + const rootDir = path.dirname(atRule.source.input.file) + const jsonFile = atRule.params.replace(/['"]+/g, "") + + const nodes = await createLeuFontStyleNodes( + path.resolve(rootDir, jsonFile), + postcss, + atRule.source + ) + + atRule.replaceWith(nodes) + }, + }, +}) + +module.exports.postcss = true diff --git a/src/components/accordion/accordion.css b/src/components/accordion/accordion.css index 831ec622..4b207b37 100644 --- a/src/components/accordion/accordion.css +++ b/src/components/accordion/accordion.css @@ -6,8 +6,8 @@ } :host { - --accordion-font-regular: var(--leu-font-regular); - --accordion-font-black: var(--leu-font-black); + --accordion-font-regular: var(--leu-font-family-regular); + --accordion-font-black: var(--leu-font-family-black); --accordion-toggle-font: var(--accordion-font-black); diff --git a/src/components/breadcrumb/breadcrumb.css b/src/components/breadcrumb/breadcrumb.css index 75ee8524..e1db73c8 100644 --- a/src/components/breadcrumb/breadcrumb.css +++ b/src/components/breadcrumb/breadcrumb.css @@ -4,8 +4,8 @@ } :host { - --breadcrumb-font-regular: var(--leu-font-regular); - --breadcrumb-font-black: var(--leu-font-black); + --breadcrumb-font-regular: var(--leu-font-family-regular); + --breadcrumb-font-black: var(--leu-font-family-black); font-family: var(--breadcrumb-font-regular); line-height: 1.5; diff --git a/src/components/button/button.css b/src/components/button/button.css index 3f9f9363..86ad9ba1 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -3,7 +3,7 @@ } button { - font-family: var(--leu-font-black); + font-family: var(--leu-font-family-black); text-align: center; appearance: none; transition: background 0.1s ease; @@ -107,7 +107,7 @@ button.ghost { background: transparent; padding: 0 0.5rem; color: var(--leu-color-black-60); - font-family: var(--leu-font-regular); + font-family: var(--leu-font-family-regular); } button.ghost:hover { diff --git a/src/components/checkbox/checkbox-group.css b/src/components/checkbox/checkbox-group.css index 0f081b1c..568a28a0 100644 --- a/src/components/checkbox/checkbox-group.css +++ b/src/components/checkbox/checkbox-group.css @@ -1,6 +1,6 @@ :host { - --group-font-regular: var(--leu-font-regular); - --group-font-black: var(--leu-font-black); + --group-font-regular: var(--leu-font-family-regular); + --group-font-black: var(--leu-font-family-black); font-family: var(--group-font-regular); } diff --git a/src/components/checkbox/checkbox.css b/src/components/checkbox/checkbox.css index a30f39d2..48f4fc53 100644 --- a/src/components/checkbox/checkbox.css +++ b/src/components/checkbox/checkbox.css @@ -8,7 +8,7 @@ --checkbox-tick-color: var(--leu-color-black-0); - --checkbox-font-regular: var(--leu-font-regular); + --checkbox-font-regular: var(--leu-font-family-regular); position: relative; diff --git a/src/components/chip/chip.css b/src/components/chip/chip.css index 133919b0..7328ee36 100644 --- a/src/components/chip/chip.css +++ b/src/components/chip/chip.css @@ -20,8 +20,8 @@ --chip-radio-background-default: var(--leu-color-black-0); --chip-radio-background-selected: var(--leu-color-func-cyan); - --chip-font-regular: var(--leu-font-regular); - --chip-font-black: var(--leu-font-black); + --chip-font-regular: var(--leu-font-family-regular); + --chip-font-black: var(--leu-font-family-black); --chip-background-color: var(--chip-background-color-default); --chip-color: var(--chip-color-default); diff --git a/src/components/input/input.css b/src/components/input/input.css index 6f3164f8..df28e496 100644 --- a/src/components/input/input.css +++ b/src/components/input/input.css @@ -26,8 +26,8 @@ --input-clear-color: var(--leu-color-black-60); - --input-font-regular: var(--leu-font-regular); - --input-font-black: var(--leu-font-black); + --input-font-regular: var(--leu-font-family-regular); + --input-font-black: var(--leu-font-family-black); display: block; font-family: var(--input-font-regular); diff --git a/src/components/menu/menu-item.css b/src/components/menu/menu-item.css index 5607f4c5..0b4493c9 100644 --- a/src/components/menu/menu-item.css +++ b/src/components/menu/menu-item.css @@ -10,10 +10,10 @@ --background-disabled: var(--leu-color-black-black-0); --color: var(--leu-color-black-transp-60); --color-disabled: var(--leu-color-black-transp-20); - --font-regular: var(--leu-font-regular); - --font-black: var(--leu-font-black); + --font-regular: var(--leu-font-family-regular); + --font-black: var(--leu-font-family-black); - font-family: var(--leu-font-regular); + font-family: var(--leu-font-family-regular); } .button { diff --git a/src/components/pagination/pagination.css b/src/components/pagination/pagination.css index 7a328095..7e8d832a 100644 --- a/src/components/pagination/pagination.css +++ b/src/components/pagination/pagination.css @@ -2,7 +2,7 @@ margin-top: 16px; display: flex; justify-content: end; - font-family: var(--leu-font-regular); + font-family: var(--leu-font-family-regular); } .input { diff --git a/src/components/popup/popup.css b/src/components/popup/popup.css index f31a98c5..30ff7bdd 100644 --- a/src/components/popup/popup.css +++ b/src/components/popup/popup.css @@ -4,8 +4,8 @@ } :host { - --popup-font-regular: var(--leu-font-regular); - --popup-font-black: var(--leu-font-black); + --popup-font-regular: var(--leu-font-family-regular); + --popup-font-black: var(--leu-font-family-black); font-family: var(--popup-font-regular); } diff --git a/src/components/radio/radio-group.css b/src/components/radio/radio-group.css index 0f081b1c..568a28a0 100644 --- a/src/components/radio/radio-group.css +++ b/src/components/radio/radio-group.css @@ -1,6 +1,6 @@ :host { - --group-font-regular: var(--leu-font-regular); - --group-font-black: var(--leu-font-black); + --group-font-regular: var(--leu-font-family-regular); + --group-font-black: var(--leu-font-family-black); font-family: var(--group-font-regular); } diff --git a/src/components/radio/radio.css b/src/components/radio/radio.css index 9bfc4036..e0f6e273 100644 --- a/src/components/radio/radio.css +++ b/src/components/radio/radio.css @@ -6,7 +6,7 @@ --radio-label-color: var(--leu-color-black-100); --radio-label-color-disabled: var(--radio-color-disabled); - --radio-font-regular: var(--leu-font-regular); + --radio-font-regular: var(--leu-font-family-regular); display: inline-flex; align-items: flex-start; diff --git a/src/components/select/select.css b/src/components/select/select.css index 0a609715..3612f552 100644 --- a/src/components/select/select.css +++ b/src/components/select/select.css @@ -26,8 +26,8 @@ --select-clear-color: var(--leu-color-black-60); - --select-font-regular: var(--leu-font-regular); - --select-font-black: var(--leu-font-black); + --select-font-regular: var(--leu-font-family-regular); + --select-font-black: var(--leu-font-family-black); --select-apply-button-color: var(--leu-color-black-100); --select-apply-button-color-focus: var(--leu-color-black-80); diff --git a/src/components/table/table.css b/src/components/table/table.css index ea0cd29e..68b366a9 100644 --- a/src/components/table/table.css +++ b/src/components/table/table.css @@ -28,7 +28,7 @@ table { border-spacing: 0; color: rgb(0 0 0 / 60%); font-size: 16px; - font-family: var(--leu-font-regular); + font-family: var(--leu-font-family-regular); line-height: 1.5; } @@ -41,7 +41,7 @@ th { text-align: left; font-size: 12px; font-weight: normal; - font-family: var(--leu-font-black); + font-family: var(--leu-font-family-black); background: var(--table-even-row-bg); } diff --git a/src/styles/custom-properties.css b/src/styles/custom-properties.css index 2f255323..458d6341 100644 --- a/src/styles/custom-properties.css +++ b/src/styles/custom-properties.css @@ -1,3 +1,5 @@ +@import url("./custom-media.css"); + :root { --leu-color-black-100: #000; --leu-color-black-80: #333; @@ -42,10 +44,12 @@ --leu-color-func-red: #d93c1a; --leu-color-func-green: #1a7f1f; - --leu-font-regular: HelveticaNowRegular, Helvetica, sans-serif; /* stylelint-disable-line value-keyword-case */ - --leu-font-black: HelveticaNowBlack, Arial Black, Helvetica, sans-serif; /* stylelint-disable-line value-keyword-case */ + --leu-font-family-regular: HelveticaNowRegular, Helvetica, sans-serif; /* stylelint-disable-line value-keyword-case */ + --leu-font-family-black: HelveticaNowBlack, Arial Black, Helvetica, sans-serif; /* stylelint-disable-line value-keyword-case */ --leu-box-shadow-short: 0px 0px 2px var(--leu-color-black-transp-40); --leu-box-shadow-regular: 0px 0px 16px var(--leu-color-black-transp-20); --leu-box-shadow-long: 0px 0px 80px var(--leu-color-black-transp-20); + + @leu-font-styles './font-definitions.json'; } diff --git a/src/styles/font-definitions.json b/src/styles/font-definitions.json new file mode 100644 index 00000000..8a5e7487 --- /dev/null +++ b/src/styles/font-definitions.json @@ -0,0 +1,202 @@ +{ + "definitions": [ + { + "fontSize": "12px", + "fontWeight": "regular", + "lineHeight": 1.5, + "spacing": "9px", + "name": "tiny" + }, + { + "fontSize": "12px", + "fontWeight": "black", + "lineHeight": 1.5, + "spacing": "9px", + "name": "tiny" + }, + { + "fontSize": "14px", + "fontWeight": "regular", + "lineHeight": 1.4, + "spacing": "10px", + "name": "small" + }, + { + "fontSize": "14px", + "fontWeight": "black", + "lineHeight": 1.4, + "spacing": "10px", + "name": "small" + }, + { + "fontSize": "16px", + "fontWeight": "regular", + "lineHeight": 1.5, + "spacing": "12px", + "name": "regular" + }, + { + "fontSize": "16px", + "fontWeight": "black", + "lineHeight": 1.5, + "spacing": "12px", + "name": "regular" + }, + { + "fontSize": "18px", + "fontWeight": "regular", + "lineHeight": 1.5, + "spacing": "13px", + "name": "biggerRegular" + }, + { + "fontSize": "18px", + "fontWeight": "black", + "lineHeight": 1.3, + "spacing": "12px", + "name": "biggerRegular" + }, + { + "fontSize": "20px", + "fontWeight": "regular", + "lineHeight": 1.5, + "spacing": "15px", + "name": "medium" + }, + { + "fontSize": "20px", + "fontWeight": "black", + "lineHeight": 1.3, + "spacing": "13px", + "name": "medium" + }, + { + "fontSize": "24px", + "fontWeight": "regular", + "lineHeight": 1.5, + "spacing": "18px", + "name": "large" + }, + { + "fontSize": "24px", + "fontWeight": "black", + "lineHeight": 1.3, + "spacing": "15px", + "name": "large" + }, + { + "fontSize": "28px", + "fontWeight": "black", + "lineHeight": 1.2, + "spacing": "17px", + "name": "smallBig" + }, + { + "fontSize": "32px", + "fontWeight": "black", + "lineHeight": 1.2, + "spacing": "19px", + "name": "big" + }, + { + "fontSize": "40px", + "fontWeight": "black", + "lineHeight": 1.2, + "spacing": "24px", + "name": "biggerBig" + }, + { + "fontSize": "48px", + "fontWeight": "black", + "lineHeight": 1, + "spacing": "24px", + "name": "smallHuge" + }, + { + "fontSize": "56px", + "fontWeight": "black", + "lineHeight": 1, + "spacing": "28px", + "name": "huge" + }, + { + "fontSize": "72px", + "fontWeight": "black", + "lineHeight": 1, + "spacing": "36px", + "name": "giant" + } + ], + "curves": [ + { + "name": "tiny", + "steps": [ + [null, "tiny"], + ["--viewport-regular", "small"] + ] + }, + { + "name": "small", + "steps": [ + [null, "small"], + ["--viewport-regular", "regular"], + ["--viewport-large", "biggerRegular"] + ] + }, + { + "name": "regular", + "steps": [ + [null, "regular"], + ["--viewport-small", "biggerRegular"], + ["--viewport-xlarge", "medium"] + ] + }, + { + "name": "biggerRegular", + "steps": [ + [null, "biggerRegular"], + ["--viewport-regular", "medium"], + ["--viewport-xlarge", "large"] + ] + }, + { + "name": "medium", + "steps": [ + [null, "biggerRegular"], + ["--viewport-small", "medium"], + ["--viewport-large", "large"], + ["--viewport-xlarge", "smallBig"] + ] + }, + { + "name": "large", + "steps": [ + [null, "biggerRegular"], + ["--viewport-regular", "large"], + ["--viewport-large", "smallBig"], + ["--viewport-xlarge", "big"] + ] + }, + { + "name": "big", + "steps": [ + [null, "large"], + ["--viewport-regular", "smallBig"], + ["--viewport-medium", "big"], + ["--viewport-large", "biggerBig"], + ["--viewport-xlarge", "smallHuge"] + ] + }, + { + "name": "huge", + "steps": [ + [null, "smallBig"], + ["--viewport-small", "big"], + ["--viewport-regular", "biggerBig"], + ["--viewport-medium", "smallHuge"], + ["--viewport-large", "huge"], + ["--viewport-xlarge", "giant"] + ] + } + ] +} diff --git a/stylelint.config.mjs b/stylelint.config.mjs index 93f4d478..1b2f461a 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -1,3 +1,4 @@ +/** @type {import('stylelint').Config} */ export default { extends: "stylelint-config-standard", overrides: [ @@ -16,6 +17,7 @@ export default { // Allowing kebab-case and BEM "selector-class-pattern": "^[a-z]([-]?[a-z0-9]+)*(__[a-z0-9]([-]?[a-z0-9]+)*)?(--[a-z0-9]([-]?[a-z0-9]+)*)?$", + "at-rule-no-unknown": [true, { ignoreAtRules: ["leu-font-styles"] }], }, ignoreFiles: ["scripts/generate-component/templates/**/*"], }