From 96298ce77c32fa8e1f59d68313c6e4eef7543c62 Mon Sep 17 00:00:00 2001 From: cheton Date: Tue, 17 Dec 2024 21:27:14 +0800 Subject: [PATCH 1/8] feat: enhance support for the nested theme token structure --- .../styled-system/src/utils/transforms.js | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/styled-system/src/utils/transforms.js b/packages/styled-system/src/utils/transforms.js index 544a54be8c..bce340eeeb 100644 --- a/packages/styled-system/src/utils/transforms.js +++ b/packages/styled-system/src/utils/transforms.js @@ -28,7 +28,43 @@ const toNegativeValue = (scale, absoluteValue, options) => { }; export const getter = (scale, value, options) => { - const result = get(scale, value); + let result = get(scale, value); + + // Extract the `value` property if the result is an object. + // + // Example usage: + // ``` + // + // + // ``` + // + // The `colors` scale in the theme: + // ```js + // { + // colors: { + // white: { + // primary: { + // value: 'rgba(255, 255, 255, .92)', + // }, + // secondary: { + // value: 'rgba(255, 255, 255, .60)', + // }, + // }, + // black: { + // primary: { + // value: 'rgba(0, 0, 0, .92)', + // }, + // secondary: { + // value: 'rgba(0, 0, 0, .65)', + // }, + // }, + // }, + // } + // ``` + if (typeof result === 'object') { + result = result?.value; + } + if (result === undefined) { return value; // fallback to value if result is undefined } From 8f6de44bf0fbd0553f12d3f0763ba813b980676b Mon Sep 17 00:00:00 2001 From: Cheton Wu Date: Wed, 18 Dec 2024 10:36:23 +0800 Subject: [PATCH 2/8] test: add tests for the `getter` transform --- .../src/utils/__tests__/transforms.test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 packages/styled-system/src/utils/__tests__/transforms.test.js diff --git a/packages/styled-system/src/utils/__tests__/transforms.test.js b/packages/styled-system/src/utils/__tests__/transforms.test.js new file mode 100644 index 0000000000..f25c9795d8 --- /dev/null +++ b/packages/styled-system/src/utils/__tests__/transforms.test.js @@ -0,0 +1,65 @@ +import { getter } from '../transforms'; + +describe('getter', () => { + const theme = { + colors: { + // flat theme tokens + 'white:primary': 'rgba(255, 255, 255, .92)', + 'white:secondary': 'rgba(255, 255, 255, .60)', + 'black:primary': 'rgba(0, 0, 0, .92)', + 'black:secondary': 'rgba(0, 0, 0, .65)', + + // nested theme tokens + white: { + primary: { + value: 'rgba(255, 255, 255, .92)', + }, + secondary: { + value: 'rgba(255, 255, 255, .60)', + }, + }, + black: { + primary: { + value: 'rgba(0, 0, 0, .92)', + }, + secondary: { + value: 'rgba(0, 0, 0, .65)', + }, + }, + }, + }; + + it('should resolve flat color tokens', () => { + expect(getter(theme.colors, 'white:primary')).toBe('rgba(255, 255, 255, .92)'); + expect(getter(theme.colors, 'white:secondary')).toBe('rgba(255, 255, 255, .60)'); + expect(getter(theme.colors, 'black:primary')).toBe('rgba(0, 0, 0, .92)'); + expect(getter(theme.colors, 'black:secondary')).toBe('rgba(0, 0, 0, .65)'); + }); + + it('should resolve nested color tokens with dot notation', () => { + expect(getter(theme.colors, 'white.primary')).toBe('rgba(255, 255, 255, .92)'); + expect(getter(theme.colors, 'white.secondary')).toBe('rgba(255, 255, 255, .60)'); + expect(getter(theme.colors, 'black.primary')).toBe('rgba(0, 0, 0, .92)'); + expect(getter(theme.colors, 'black.secondary')).toBe('rgba(0, 0, 0, .65)'); + }); + + it('should fallback to original value when token path does not exist', () => { + expect(getter(theme.colors, 'white')).toBe('white'); + }); + + it('should handle undefined theme values', () => { + expect(getter(undefined, 'white.undefined')).toBe('white.undefined'); + }); + + it('should handle nested objects without value property', () => { + const customTheme = { + colors: { + custom: { + primary: 'rgb(100, 100, 100)', + }, + }, + }; + const result = getter(customTheme.colors, 'custom.primary'); + expect(result).toBe('rgb(100, 100, 100)'); + }); +}); From 9855f908f1a535e90722e9ef3bcbce8f8b16025a Mon Sep 17 00:00:00 2001 From: Cheton Wu Date: Wed, 18 Dec 2024 11:31:49 +0800 Subject: [PATCH 3/8] feat(styled-system): enhance folder structure for better readability --- packages/styled-system/package.json | 3 + .../styled-system/src/config/background.js | 2 +- packages/styled-system/src/config/border.js | 2 +- packages/styled-system/src/config/margin.js | 2 +- packages/styled-system/src/config/outline.js | 4 +- packages/styled-system/src/config/position.js | 2 +- packages/styled-system/src/config/scroll.js | 2 +- packages/styled-system/src/config/shape.js | 2 +- packages/styled-system/src/config/text.js | 2 +- packages/styled-system/src/core/system.js | 4 +- packages/styled-system/src/sx.js | 2 +- .../__tests__/getter.test.js} | 2 +- .../__tests__/positiveOrNegative.test.js | 42 +++++ .../styled-system/src/transforms/getter.js | 93 ++++++++++ .../src/transforms/positiveOrNegative.js | 123 +++++++++++++ packages/styled-system/src/utils/css-vars.js | 21 --- .../styled-system/src/utils/ensure-array.js | 9 - .../styled-system/src/utils/transforms.js | 170 ------------------ 18 files changed, 273 insertions(+), 214 deletions(-) rename packages/styled-system/src/{utils/__tests__/transforms.test.js => transforms/__tests__/getter.test.js} (98%) create mode 100644 packages/styled-system/src/transforms/__tests__/positiveOrNegative.test.js create mode 100644 packages/styled-system/src/transforms/getter.js create mode 100644 packages/styled-system/src/transforms/positiveOrNegative.js delete mode 100644 packages/styled-system/src/utils/css-vars.js delete mode 100644 packages/styled-system/src/utils/ensure-array.js delete mode 100644 packages/styled-system/src/utils/transforms.js diff --git a/packages/styled-system/package.json b/packages/styled-system/package.json index 1bfbd9a6f3..a766b99e97 100644 --- a/packages/styled-system/package.json +++ b/packages/styled-system/package.json @@ -19,6 +19,9 @@ "prepublish": "yarn run build", "test": "jest --maxWorkers=2" }, + "dependencies": { + "ensure-type": "^1.5.1" + }, "devDependencies": { "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", diff --git a/packages/styled-system/src/config/background.js b/packages/styled-system/src/config/background.js index 0a796576d2..a9162cb6f5 100644 --- a/packages/styled-system/src/config/background.js +++ b/packages/styled-system/src/config/background.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'background'; const config = { diff --git a/packages/styled-system/src/config/border.js b/packages/styled-system/src/config/border.js index 2bcea63080..69c79c9de5 100644 --- a/packages/styled-system/src/config/border.js +++ b/packages/styled-system/src/config/border.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const _border = { /** diff --git a/packages/styled-system/src/config/margin.js b/packages/styled-system/src/config/margin.js index b102f23295..f46a02a9a3 100644 --- a/packages/styled-system/src/config/margin.js +++ b/packages/styled-system/src/config/margin.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'margin'; const config = { diff --git a/packages/styled-system/src/config/outline.js b/packages/styled-system/src/config/outline.js index 1c7f35d6b9..78460be707 100644 --- a/packages/styled-system/src/config/outline.js +++ b/packages/styled-system/src/config/outline.js @@ -1,7 +1,5 @@ import system from '../core/system'; -import { - positiveOrNegative as positiveOrNegativeTransform, -} from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'outline'; const config = { diff --git a/packages/styled-system/src/config/position.js b/packages/styled-system/src/config/position.js index 15f3927c78..0dfa6b2ca5 100644 --- a/packages/styled-system/src/config/position.js +++ b/packages/styled-system/src/config/position.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'position'; const config = { diff --git a/packages/styled-system/src/config/scroll.js b/packages/styled-system/src/config/scroll.js index 7cf91ec61e..ba5df87c19 100644 --- a/packages/styled-system/src/config/scroll.js +++ b/packages/styled-system/src/config/scroll.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'scroll'; const config = { diff --git a/packages/styled-system/src/config/shape.js b/packages/styled-system/src/config/shape.js index 159648459b..6c9692fd87 100644 --- a/packages/styled-system/src/config/shape.js +++ b/packages/styled-system/src/config/shape.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; /** * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Shapes diff --git a/packages/styled-system/src/config/text.js b/packages/styled-system/src/config/text.js index 6601139da8..e6bc7398ae 100644 --- a/packages/styled-system/src/config/text.js +++ b/packages/styled-system/src/config/text.js @@ -1,5 +1,5 @@ import system from '../core/system'; -import { positiveOrNegative as positiveOrNegativeTransform } from '../utils/transforms'; +import positiveOrNegativeTransform from '../transforms/positiveOrNegative'; const group = 'text'; const config = { diff --git a/packages/styled-system/src/core/system.js b/packages/styled-system/src/core/system.js index a755a99804..be9f47cb69 100644 --- a/packages/styled-system/src/core/system.js +++ b/packages/styled-system/src/core/system.js @@ -1,5 +1,5 @@ -import ensureArray from '../utils/ensure-array'; -import { getter as getterTransform } from '../utils/transforms'; +import { ensureArray } from 'ensure-type'; +import getterTransform from '../transforms/getter'; import parser from './parser'; const system = (config, options) => { diff --git a/packages/styled-system/src/sx.js b/packages/styled-system/src/sx.js index de775d6e45..3ee49128ca 100644 --- a/packages/styled-system/src/sx.js +++ b/packages/styled-system/src/sx.js @@ -1,5 +1,5 @@ +import { ensureArray } from 'ensure-type'; import system from './system'; -import ensureArray from './utils/ensure-array'; import get from './utils/get'; import { pseudoClassSelector, pseudoElementSelector } from './pseudo'; diff --git a/packages/styled-system/src/utils/__tests__/transforms.test.js b/packages/styled-system/src/transforms/__tests__/getter.test.js similarity index 98% rename from packages/styled-system/src/utils/__tests__/transforms.test.js rename to packages/styled-system/src/transforms/__tests__/getter.test.js index f25c9795d8..645ce89ae4 100644 --- a/packages/styled-system/src/utils/__tests__/transforms.test.js +++ b/packages/styled-system/src/transforms/__tests__/getter.test.js @@ -1,4 +1,4 @@ -import { getter } from '../transforms'; +import getter from '../getter'; describe('getter', () => { const theme = { diff --git a/packages/styled-system/src/transforms/__tests__/positiveOrNegative.test.js b/packages/styled-system/src/transforms/__tests__/positiveOrNegative.test.js new file mode 100644 index 0000000000..33902def51 --- /dev/null +++ b/packages/styled-system/src/transforms/__tests__/positiveOrNegative.test.js @@ -0,0 +1,42 @@ +import positiveOrNegative from '../positiveOrNegative'; + +describe('positiveOrNegative', () => { + const theme = { + sizes: { + '1x': '.25rem', + '2x': '.5rem', + '3x': '.75rem', + '4x': '1rem', + }, + }; + + it('should handle positive values', () => { + expect(positiveOrNegative(theme.sizes, '1x')).toBe('.25rem'); + expect(positiveOrNegative(theme.sizes, '2x')).toBe('.5rem'); + expect(positiveOrNegative(theme.sizes, '3x')).toBe('.75rem'); + expect(positiveOrNegative(theme.sizes, '4x')).toBe('1rem'); + }); + + it('should handle negative values', () => { + expect(positiveOrNegative(theme.sizes, '-1x')).toBe('-.25rem'); + expect(positiveOrNegative(theme.sizes, '-2x')).toBe('-.5rem'); + expect(positiveOrNegative(theme.sizes, '-3x')).toBe('-.75rem'); + expect(positiveOrNegative(theme.sizes, '-4x')).toBe('-1rem'); + }); + + it('should return original value when theme.sizes value is not found', () => { + expect(positiveOrNegative(theme.sizes, '5x')).toBe('5x'); + expect(positiveOrNegative(theme.sizes, '-5x')).toBe('-5x'); + }); + + it('should handle undefined theme.sizes', () => { + expect(positiveOrNegative(undefined, '2x')).toBe('2x'); + expect(positiveOrNegative(undefined, '-2x')).toBe('-2x'); + }); + + it('should handle non-numeric strings', () => { + expect(positiveOrNegative(theme.sizes, 'auto')).toBe('auto'); + expect(positiveOrNegative(theme.sizes, '4px')).toBe('4px'); + expect(positiveOrNegative(theme.sizes, '-4px')).toBe('-4px'); + }); +}); diff --git a/packages/styled-system/src/transforms/getter.js b/packages/styled-system/src/transforms/getter.js new file mode 100644 index 0000000000..13a83c535c --- /dev/null +++ b/packages/styled-system/src/transforms/getter.js @@ -0,0 +1,93 @@ +import get from '../utils/get'; + +/** + * Returns a CSS variable name formatted from the given name and options. + * + * @param {string} name - The name of the variable. + * @param {object} [options] - The options object. + * @param {string} [options.prefix=''] - The prefix to use for the variable name. + * @param {string} [options.delimiter='-'] - The delimiter to use between the prefix and name. + * + * @return {string} The CSS variable name. +*/ +const toCSSVariable = (name, options) => { + const { + prefix = '', + delimiter = '-', + } = { ...options }; + const variableName = ([prefix, name].filter(Boolean).join(delimiter)) + .replace(/\s+/g, delimiter) // replace whitespace characters + .replace(/[^a-zA-Z0-9-_]/g, delimiter) // replace non-alphanumeric, non-hyphen, non-underscore characters + .replace(/^-+|-+$/g, ''); // trim hyphens from beginning and end of string + return `--${variableName}`; +}; + +const getter = (scale, value, options) => { + let result = get(scale, value); + + // Extract the `value` property if the result is an object. + // + // Example usage: + // ``` + // + // + // ``` + // + // The `colors` scale in the theme: + // ```js + // { + // colors: { + // white: { + // primary: { + // value: 'rgba(255, 255, 255, .92)', + // }, + // secondary: { + // value: 'rgba(255, 255, 255, .60)', + // }, + // }, + // black: { + // primary: { + // value: 'rgba(0, 0, 0, .92)', + // }, + // secondary: { + // value: 'rgba(0, 0, 0, .65)', + // }, + // }, + // }, + // } + // ``` + if (typeof result === 'object') { + result = result?.value; + } + + if (result === undefined) { + return value; // fallback to value if result is undefined + } + + const theme = options?.props?.theme; + // FIXME: `theme.config.prefix` and `theme.__cssVariableMap` are deprecated and will be removed in the next major release + const hasCSSVariables = !!(theme?.cssVariables ?? theme?.__cssVariableMap); + if (hasCSSVariables) { + const cssVariablePrefix = (theme?.cssVariablePrefix) ?? (theme?.config?.prefix); + const cssVariables = (theme?.cssVariables) ?? (theme?.__cssVariableMap); + const contextScale = options?.context?.scale; + const cssVariable = toCSSVariable( + // | contextScale | value | + // | ------------ | --------- | + // | colors | 'blue:50' | + // | space | 0 | + [contextScale, String(value ?? '')].filter(Boolean).join('.'), // => 'colors.blue:50' + { prefix: cssVariablePrefix, delimiter: '-' }, + ); // => '--tonic-colors-blue-50' + const cssVariableValue = cssVariables?.[cssVariable]; // => '#578aef' + if (cssVariableValue !== undefined) { + // => Replace '#578aef' with 'var(--tonic-colors-blue-50)' + return String(result ?? '').replaceAll(cssVariableValue, `var(${cssVariable})`); + } + // fallback to the original result + } + + return result; +}; + +export default getter; diff --git a/packages/styled-system/src/transforms/positiveOrNegative.js b/packages/styled-system/src/transforms/positiveOrNegative.js new file mode 100644 index 0000000000..8501f397e1 --- /dev/null +++ b/packages/styled-system/src/transforms/positiveOrNegative.js @@ -0,0 +1,123 @@ +import getter from './getter'; + +const hasOwnSafe = (obj, key) => { + if (obj === undefined || obj === null) { + return false; + } + + return Object.hasOwn + ? Object.hasOwn(obj, key) + : Object.prototype.hasOwnProperty.call(obj, key); +}; + +// Check if a value is a simple CSS variable +// e.g. var(--tonic-spacing-1) +const isSimpleCSSVariable = (value) => { + const re = /^var\(\s*([a-zA-Z0-9\-_]+)\s*\)$/; + return re.test(String(value ?? '').trim()); +}; + +// Negate the value, handling CSS variables and numeric values +const toNegativeValue = (scale, absoluteValue, options) => { + const theme = options?.props?.theme; + const n = getter(scale, absoluteValue, options); + + // Handle CSS variables for negative values + if (!!theme?.cssVariables && isSimpleCSSVariable(n)) { + // https://stackoverflow.com/questions/49469344/using-negative-css-custom-properties + return `calc(0px - ${n})`; + } + + // Handle numeric value + if (typeof n === 'number' && Number.isFinite(n)) { + return n * -1; + } + + return `-${n}`; +}; + +const positiveOrNegative = (scale, value, options) => { + /** + * Scale object + * + * ```js + * { + * '1x': '0.25rem', + * '2x': 8, + * } + * ``` + * + * Example + * + * ```jsx + * + * // => margin: 0.25rem + * + * // => margin: 8px + * + * // => margin: -0.25rem + * + * // => margin: -8px + * ``` + */ + if (typeof value === 'string') { + const absoluteValue = (value.startsWith('+') || value.startsWith('-')) ? value.slice(1) : value; + const isNonNegative = !value.startsWith('-'); + + // Return the result if the scale object does not contain the absolute value + if (!hasOwnSafe(scale, absoluteValue)) { + return getter(scale, value, options); + } + + // Return the result if the value is non-negative + if (isNonNegative) { + return getter(scale, value, options); + } + + return toNegativeValue(scale, absoluteValue, options); + } + + /** + * Scale object + * + * ```js + * { + * 4: '0.25rem', + * 8: 8, + * } + * ``` + * + * Example + * + * ```jsx + * + * // => margin: 0.25rem + * + * // => margin: 8px + * + * // => margin: -0.25rem + * + * // => margin: -8px + * ``` + */ + if (typeof value === 'number' && Number.isFinite(value)) { + const absoluteValue = Math.abs(value); + const isNonNegative = !(value < 0); + + // Return the result if the scale object does not contain the absolute value + if (!hasOwnSafe(scale, absoluteValue)) { + return getter(scale, value, options); + } + + // Return the result if the value is non-negative + if (isNonNegative) { + return getter(scale, value, options); + } + + return toNegativeValue(scale, absoluteValue, options); + } + + return getter(scale, value, options); +}; + +export default positiveOrNegative; diff --git a/packages/styled-system/src/utils/css-vars.js b/packages/styled-system/src/utils/css-vars.js deleted file mode 100644 index b7c0dfd441..0000000000 --- a/packages/styled-system/src/utils/css-vars.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Returns a CSS variable name formatted from the given name and options. - * - * @param {string} name - The name of the variable. - * @param {object} [options] - The options object. - * @param {string} [options.prefix=''] - The prefix to use for the variable name. - * @param {string} [options.delimiter='-'] - The delimiter to use between the prefix and name. - * - * @return {string} The CSS variable name. -*/ -export const toCSSVariable = (name, options) => { - const { - prefix = '', - delimiter = '-', - } = { ...options }; - const variableName = ([prefix, name].filter(Boolean).join(delimiter)) - .replace(/\s+/g, delimiter) // replace whitespace characters - .replace(/[^a-zA-Z0-9-_]/g, delimiter) // replace non-alphanumeric, non-hyphen, non-underscore characters - .replace(/^-+|-+$/g, ''); // trim hyphens from beginning and end of string - return `--${variableName}`; -}; diff --git a/packages/styled-system/src/utils/ensure-array.js b/packages/styled-system/src/utils/ensure-array.js deleted file mode 100644 index 4fd7c3b617..0000000000 --- a/packages/styled-system/src/utils/ensure-array.js +++ /dev/null @@ -1,9 +0,0 @@ -const ensureArray = (value, defaultValue = []) => { - if (value === undefined || value === null) { - return [].concat(defaultValue); - } - - return Array.isArray(value) ? value : [].concat(value); -}; - -export default ensureArray; diff --git a/packages/styled-system/src/utils/transforms.js b/packages/styled-system/src/utils/transforms.js deleted file mode 100644 index bce340eeeb..0000000000 --- a/packages/styled-system/src/utils/transforms.js +++ /dev/null @@ -1,170 +0,0 @@ -import get from './get'; -import { toCSSVariable } from './css-vars'; - -// Check if a value is a simple CSS variable -// e.g. var(--tonic-spacing-1) -const isSimpleCSSVariable = (value) => { - const re = /^var\(\s*([a-zA-Z0-9\-_]+)\s*\)$/; - return re.test(String(value ?? '').trim()); -}; - -// Negate the value, handling CSS variables and numeric values -const toNegativeValue = (scale, absoluteValue, options) => { - const theme = options?.props?.theme; - const n = getter(scale, absoluteValue, options); - - // Handle CSS variables for negative values - if (!!theme?.cssVariables && isSimpleCSSVariable(n)) { - // https://stackoverflow.com/questions/49469344/using-negative-css-custom-properties - return `calc(0px - ${n})`; - } - - // Handle numeric value - if (typeof n === 'number' && Number.isFinite(n)) { - return n * -1; - } - - return `-${n}`; -}; - -export const getter = (scale, value, options) => { - let result = get(scale, value); - - // Extract the `value` property if the result is an object. - // - // Example usage: - // ``` - // - // - // ``` - // - // The `colors` scale in the theme: - // ```js - // { - // colors: { - // white: { - // primary: { - // value: 'rgba(255, 255, 255, .92)', - // }, - // secondary: { - // value: 'rgba(255, 255, 255, .60)', - // }, - // }, - // black: { - // primary: { - // value: 'rgba(0, 0, 0, .92)', - // }, - // secondary: { - // value: 'rgba(0, 0, 0, .65)', - // }, - // }, - // }, - // } - // ``` - if (typeof result === 'object') { - result = result?.value; - } - - if (result === undefined) { - return value; // fallback to value if result is undefined - } - - const theme = options?.props?.theme; - // FIXME: `theme.config.prefix` and `theme.__cssVariableMap` are deprecated and will be removed in the next major release - const hasCSSVariables = !!(theme?.cssVariables ?? theme?.__cssVariableMap); - if (hasCSSVariables) { - const cssVariablePrefix = (theme?.cssVariablePrefix) ?? (theme?.config?.prefix); - const cssVariables = (theme?.cssVariables) ?? (theme?.__cssVariableMap); - const contextScale = options?.context?.scale; - const cssVariable = toCSSVariable( - // | contextScale | value | - // | ------------ | --------- | - // | colors | 'blue:50' | - // | space | 0 | - [contextScale, String(value ?? '')].filter(Boolean).join('.'), // => 'colors.blue:50' - { prefix: cssVariablePrefix, delimiter: '-' }, - ); // => '--tonic-colors-blue-50' - const cssVariableValue = cssVariables?.[cssVariable]; // => '#578aef' - if (cssVariableValue !== undefined) { - // => Replace '#578aef' with 'var(--tonic-colors-blue-50)' - return String(result ?? '').replaceAll(cssVariableValue, `var(${cssVariable})`); - } - // fallback to the original result - } - - return result; -}; - -export const positiveOrNegative = (scale, value, options) => { - /** - * Scale object - * - * ```js - * { - * '1x': '0.25rem', - * '2x': 8, - * } - * ``` - * - * Example - * - * ```jsx - * - * // => margin: 0.25rem - * - * // => margin: 8px - * - * // => margin: -0.25rem - * - * // => margin: -8px - * ``` - */ - if (typeof value === 'string') { - const absoluteValue = (value.startsWith('+') || value.startsWith('-')) ? value.slice(1) : value; - const isNonNegative = !value.startsWith('-'); - - // Return the result if the value is non-negative or if the scale object does not contain the absolute value - if (isNonNegative || !Object.prototype.hasOwnProperty.call(scale, absoluteValue)) { - return getter(scale, value, options); - } - - return toNegativeValue(scale, absoluteValue, options); - } - - /** - * Scale object - * - * ```js - * { - * 4: '0.25rem', - * 8: 8, - * } - * ``` - * - * Example - * - * ```jsx - * - * // => margin: 0.25rem - * - * // => margin: 8px - * - * // => margin: -0.25rem - * - * // => margin: -8px - * ``` - */ - if (typeof value === 'number' && Number.isFinite(value)) { - const absoluteValue = Math.abs(value); - const isNonNegative = !(value < 0); - - // Return the result if the value is non-negative or if the scale object does not contain the absolute value - if (isNonNegative || !Object.prototype.hasOwnProperty.call(scale, absoluteValue)) { - return getter(scale, value, options); - } - - return toNegativeValue(scale, absoluteValue, options); - } - - return getter(scale, value, options); -}; From d375c05dbc5354deb960d6323bbdd3097e57f6e6 Mon Sep 17 00:00:00 2001 From: Cheton Wu <447801+cheton@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:58:12 +0800 Subject: [PATCH 4/8] Create tonic-ui-956.md --- .changeset/tonic-ui-956.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tonic-ui-956.md diff --git a/.changeset/tonic-ui-956.md b/.changeset/tonic-ui-956.md new file mode 100644 index 0000000000..f2beb36570 --- /dev/null +++ b/.changeset/tonic-ui-956.md @@ -0,0 +1,5 @@ +--- +"@tonic-ui/styled-system": patch +--- + +feat: enhance support for the nested theme token structure From 4c99f660e16d4c651213c81f64b680fb4f4b6cdd Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 18 Dec 2024 16:41:19 +0800 Subject: [PATCH 5/8] chore: update yarn.lock file --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index b41326ffe6..f1dbc53e30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4758,6 +4758,7 @@ __metadata: "@trendmicro/babel-config": ^1.0.2 cross-env: ^7.0.3 del-cli: ^5.0.0 + ensure-type: ^1.5.1 eslint: ^8.25.0 eslint-config-trendmicro: ^3.0.0 eslint-plugin-import: latest From 8c1dc7bf97e379d8c2c26a9a2b8ccabc4094dd6b Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 18 Dec 2024 19:10:51 +0800 Subject: [PATCH 6/8] feat: import `isNullish` and `merge` from `@tonic-ui/utils` --- packages/styled-system/package.json | 1 + packages/styled-system/src/core/parser.js | 107 ++++++++++-------- packages/styled-system/src/core/system.js | 3 +- packages/styled-system/src/sx.js | 7 +- .../src/transforms/positiveOrNegative.js | 3 +- .../src/utils/__tests__/get.test.js | 90 ++++++++++----- .../src/utils/__tests__/merge-object.test.js | 26 ----- packages/styled-system/src/utils/get.js | 16 ++- .../styled-system/src/utils/merge-object.js | 21 ---- .../styled-system/src/utils/sort-object.js | 17 --- 10 files changed, 141 insertions(+), 150 deletions(-) delete mode 100644 packages/styled-system/src/utils/__tests__/merge-object.test.js delete mode 100644 packages/styled-system/src/utils/merge-object.js delete mode 100644 packages/styled-system/src/utils/sort-object.js diff --git a/packages/styled-system/package.json b/packages/styled-system/package.json index a766b99e97..849c277a28 100644 --- a/packages/styled-system/package.json +++ b/packages/styled-system/package.json @@ -20,6 +20,7 @@ "test": "jest --maxWorkers=2" }, "dependencies": { + "@tonic-ui/utils": "^2.1.1", "ensure-type": "^1.5.1" }, "devDependencies": { diff --git a/packages/styled-system/src/core/parser.js b/packages/styled-system/src/core/parser.js index 88e3b96524..d95a67335e 100644 --- a/packages/styled-system/src/core/parser.js +++ b/packages/styled-system/src/core/parser.js @@ -1,10 +1,67 @@ +import { merge } from '@tonic-ui/utils'; import get from '../utils/get'; -import mergeObject from '../utils/merge-object'; -import sortObject from '../utils/sort-object'; const defaultBreakpoints = []; const createMediaQuery = n => `@media screen and (min-width: ${n})`; +const parseResponsiveStyle = (mediaQueries, sx, scale, raw, _props) => { + const styles = {}; + + raw.slice(0, mediaQueries.length).forEach((value, i) => { + const media = mediaQueries[i]; + const style = sx(scale, value, _props); + if (!media) { + Object.assign(styles, style); + } else { + Object.assign(styles, { + [media]: Object.assign({}, styles[media], style), + }); + } + }); + + return styles; +}; + +const parseResponsiveObject = (breakpoints, sx, scale, raw, _props) => { + const styles = {}; + + for (let key in raw) { + if (!Object.prototype.hasOwnProperty.call(raw, key)) { + continue; + } + + const breakpoint = breakpoints[key]; + const value = raw[key]; + const style = sx(scale, value, _props); + if (!breakpoint) { + Object.assign(styles, style); + } else { + const media = createMediaQuery(breakpoint); + Object.assign(styles, { + [media]: Object.assign({}, styles[media], style), + }); + } + } + + return styles; +}; + +// sort object-value responsive styles +const sortObject = obj => { + const next = {}; + + Object.keys(obj) + .sort((a, b) => a.localeCompare(b, undefined, { + numeric: true, + sensitivity: 'base', + })) + .forEach(key => { + next[key] = obj[key]; + }); + + return next; +}; + const parser = config => { const cache = {}; const parse = props => { @@ -29,14 +86,14 @@ const parser = config => { null, ...cache.breakpoints.map(createMediaQuery), ]; - styles = mergeObject( + styles = merge( styles, parseResponsiveStyle(cache.media, sx, scale, raw, props) ); continue; } if (raw !== null) { - styles = mergeObject( + styles = merge( styles, parseResponsiveObject(cache.breakpoints, sx, scale, raw, props) ); @@ -72,46 +129,4 @@ const parser = config => { return parse; }; -const parseResponsiveStyle = (mediaQueries, sx, scale, raw, _props) => { - const styles = {}; - - raw.slice(0, mediaQueries.length).forEach((value, i) => { - const media = mediaQueries[i]; - const style = sx(scale, value, _props); - if (!media) { - Object.assign(styles, style); - } else { - Object.assign(styles, { - [media]: Object.assign({}, styles[media], style), - }); - } - }); - - return styles; -}; - -const parseResponsiveObject = (breakpoints, sx, scale, raw, _props) => { - const styles = {}; - - for (let key in raw) { - if (!Object.prototype.hasOwnProperty.call(raw, key)) { - continue; - } - - const breakpoint = breakpoints[key]; - const value = raw[key]; - const style = sx(scale, value, _props); - if (!breakpoint) { - Object.assign(styles, style); - } else { - const media = createMediaQuery(breakpoint); - Object.assign(styles, { - [media]: Object.assign({}, styles[media], style), - }); - } - } - - return styles; -}; - export default parser; diff --git a/packages/styled-system/src/core/system.js b/packages/styled-system/src/core/system.js index be9f47cb69..7af81e1885 100644 --- a/packages/styled-system/src/core/system.js +++ b/packages/styled-system/src/core/system.js @@ -1,3 +1,4 @@ +import { isNullish } from '@tonic-ui/utils'; import { ensureArray } from 'ensure-type'; import getterTransform from '../transforms/getter'; import parser from './parser'; @@ -65,7 +66,7 @@ const createStyleFunction = ({ const sx = (scale, value, props) => { const transformOptions = { context, props }; const transformedValue = transform(scale, value, transformOptions); - if (transformedValue === null || transformedValue === undefined) { + if (isNullish(transformedValue)) { return {}; } diff --git a/packages/styled-system/src/sx.js b/packages/styled-system/src/sx.js index 3ee49128ca..8bd43bf85b 100644 --- a/packages/styled-system/src/sx.js +++ b/packages/styled-system/src/sx.js @@ -1,10 +1,11 @@ +import { isNullish } from '@tonic-ui/utils'; import { ensureArray } from 'ensure-type'; import system from './system'; import get from './utils/get'; import { pseudoClassSelector, pseudoElementSelector } from './pseudo'; const createPseudoResolver = (theme) => (styleProps) => { - if (styleProps === null || styleProps === undefined) { + if (isNullish(styleProps)) { return {}; } @@ -69,7 +70,7 @@ const createResponsiveResolver = theme => styleProps => { } const value = typeof styleProps[key] === 'function' ? styleProps[key](theme) : styleProps[key]; - if (value === null || value === undefined) { + if (isNullish(value)) { continue; } @@ -96,7 +97,7 @@ const createResponsiveResolver = theme => styleProps => { }; const sx = (valueOrFn) => (props = {}) => { - if (valueOrFn === null || valueOrFn === undefined) { + if (isNullish(valueOrFn)) { return {}; } diff --git a/packages/styled-system/src/transforms/positiveOrNegative.js b/packages/styled-system/src/transforms/positiveOrNegative.js index 8501f397e1..49c884bc52 100644 --- a/packages/styled-system/src/transforms/positiveOrNegative.js +++ b/packages/styled-system/src/transforms/positiveOrNegative.js @@ -1,7 +1,8 @@ +import { isNullish } from '@tonic-ui/utils'; import getter from './getter'; const hasOwnSafe = (obj, key) => { - if (obj === undefined || obj === null) { + if (isNullish(obj)) { return false; } diff --git a/packages/styled-system/src/utils/__tests__/get.test.js b/packages/styled-system/src/utils/__tests__/get.test.js index 610476c55a..a3c5b03080 100644 --- a/packages/styled-system/src/utils/__tests__/get.test.js +++ b/packages/styled-system/src/utils/__tests__/get.test.js @@ -1,38 +1,68 @@ import get from '../get'; -test('returns a deeply nested value', () => { - const a = get( - { - colors: { - blue: ['#0cf', '#0be', '#09d', '#07c'], - }, - }, - 'colors.blue.3' - ); - expect(a).toBe('#07c'); -}); +describe('get function tests', () => { + describe('Default Value Handling', () => { + const obj = { a: { b: { c: 42 } } }; + const defaultValue = 'default'; -test('supports fallback values', () => { - const a = get({}, 'hi', 'nope'); - expect(a).toBe('nope'); -}); + test('returns default value for missing paths', () => { + expect(get(obj, 'a.b.c.d', defaultValue)).toBe(defaultValue); + expect(get(obj, ['a', 'b', 'c', 'd'], defaultValue)).toBe(defaultValue); + expect(get(obj, 'a.b.x', defaultValue)).toBe(defaultValue); + }); -test('handles number values', () => { - const a = get([1, 2, 3], 0); - expect(a).toBe(1); -}); + test('returns default value for null or undefined input', () => { + expect(get(obj, null, defaultValue)).toBe(defaultValue); + expect(get(obj, undefined, defaultValue)).toBe(defaultValue); + expect(get(null, 'a.b.c', defaultValue)).toBe(defaultValue); + expect(get(undefined, 'a.b.c', defaultValue)).toBe(defaultValue); + }); -test('handles undefined values', () => { - const a = get({}, undefined); - expect(a).toBe(undefined); -}); + test('returns default value for empty or undefined paths', () => { + expect(get(obj, '', defaultValue)).toBe(defaultValue); + expect(get(obj, ['a', 'b', 'x'], defaultValue)).toBe(defaultValue); + expect(get({ a: { b: undefined } }, 'a.b', defaultValue)).toBe(defaultValue); + }); + }); -test('handles null values', () => { - const a = get({}, null); - expect(a).toBe(undefined); -}); + describe('Value Retrieval', () => { + test('returns a deeply nested value', () => { + const result = get( + { + colors: { + blue: ['#0cf', '#0be', '#09d', '#07c'], + }, + }, + 'colors.blue.3' + ); + expect(result).toBe('#07c'); + }); + + test('supports fallback values', () => { + const result = get({}, 'hi', 'nope'); + expect(result).toBe('nope'); + }); + + test('handles number indices in arrays', () => { + const result = get([1, 2, 3], 0); + expect(result).toBe(1); + }); + + test('returns 0 index items from arrays', () => { + const result = get(['a', 'b', 'c'], 0); + expect(result).toBe('a'); + }); + }); + + describe('Edge Case Handling', () => { + test('handles undefined values gracefully', () => { + const result = get({}, undefined); + expect(result).toBe(undefined); + }); -test('returns 0 index items', () => { - const a = get(['a', 'b', 'c'], 0); - expect(a).toBe('a'); + test('handles null values gracefully', () => { + const result = get({}, null); + expect(result).toBe(undefined); + }); + }); }); diff --git a/packages/styled-system/src/utils/__tests__/merge-object.test.js b/packages/styled-system/src/utils/__tests__/merge-object.test.js deleted file mode 100644 index d6bac4d011..0000000000 --- a/packages/styled-system/src/utils/__tests__/merge-object.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import mergeObject from '../merge-object'; - -test('deeply merges', () => { - const result = mergeObject( - { - hello: 'hi', - media: { - howdy: 'ho', - }, - }, - { - beep: 'boop', - media: { - bleep: 'bloop', - }, - } - ); - expect(result).toEqual({ - hello: 'hi', - beep: 'boop', - media: { - howdy: 'ho', - bleep: 'bloop', - }, - }); -}); diff --git a/packages/styled-system/src/utils/get.js b/packages/styled-system/src/utils/get.js index 8d006f8014..d19102f65c 100644 --- a/packages/styled-system/src/utils/get.js +++ b/packages/styled-system/src/utils/get.js @@ -1,10 +1,16 @@ // based on https://github.com/developit/dlv -const get = (obj, key, def, p, undef) => { - key = key && key.split ? key.split('.') : [key]; - for (p = 0; p < key.length; p++) { - obj = obj ? obj[key[p]] : undef; +const get = (obj, key, defaultValue, undef) => { + if (key && key.split) { + key = key.split('.'); + } else { + key = Array.isArray(key) ? key : [key]; } - return obj === undef ? def : obj; + + for (let i = 0; i < key.length; ++i) { + obj = obj ? obj[key[i]] : undef; + } + + return obj === undef ? defaultValue : obj; }; export default get; diff --git a/packages/styled-system/src/utils/merge-object.js b/packages/styled-system/src/utils/merge-object.js deleted file mode 100644 index 9653928819..0000000000 --- a/packages/styled-system/src/utils/merge-object.js +++ /dev/null @@ -1,21 +0,0 @@ -const mergeObject = (a, b) => { - const result = Object.assign({}, a, b); - - for (const key in a) { - if (!Object.prototype.hasOwnProperty.call(a, key)) { - continue; - } - - if (!a[key] || typeof b[key] !== 'object') { - continue; - } - - Object.assign(result, { - [key]: Object.assign(a[key], b[key]), - }); - } - - return result; -}; - -export default mergeObject; diff --git a/packages/styled-system/src/utils/sort-object.js b/packages/styled-system/src/utils/sort-object.js deleted file mode 100644 index c20b07cc29..0000000000 --- a/packages/styled-system/src/utils/sort-object.js +++ /dev/null @@ -1,17 +0,0 @@ -// sort object-value responsive styles -const sortObject = obj => { - const next = {}; - - Object.keys(obj) - .sort((a, b) => a.localeCompare(b, undefined, { - numeric: true, - sensitivity: 'base', - })) - .forEach(key => { - next[key] = obj[key]; - }); - - return next; -}; - -export default sortObject; From 01e6c20888669f0baf17ddb24acef9cb6fbfd3f3 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 18 Dec 2024 19:11:19 +0800 Subject: [PATCH 7/8] chore: update yarn.lock file --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index f1dbc53e30..619389a38b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4755,6 +4755,7 @@ __metadata: "@codecov/rollup-plugin": ^1.5.1 "@rollup/plugin-babel": ^6.0.0 "@rollup/plugin-node-resolve": ^15.0.0 + "@tonic-ui/utils": ^2.1.1 "@trendmicro/babel-config": ^1.0.2 cross-env: ^7.0.3 del-cli: ^5.0.0 From 562b05f6d473294dd9b1eada3790f084febc4fb5 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 18 Dec 2024 19:13:54 +0800 Subject: [PATCH 8/8] chore: update tonic-ui-956.md --- .changeset/tonic-ui-956.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tonic-ui-956.md b/.changeset/tonic-ui-956.md index f2beb36570..58416b4632 100644 --- a/.changeset/tonic-ui-956.md +++ b/.changeset/tonic-ui-956.md @@ -1,5 +1,5 @@ --- -"@tonic-ui/styled-system": patch +"@tonic-ui/styled-system": minor --- feat: enhance support for the nested theme token structure