diff --git a/.changeset/rotten-beers-hunt.md b/.changeset/rotten-beers-hunt.md new file mode 100644 index 0000000000..81d8a516aa --- /dev/null +++ b/.changeset/rotten-beers-hunt.md @@ -0,0 +1,6 @@ +--- +"@marigold/components": minor +"@marigold/system": minor +--- + +feat: `getColor` util diff --git a/packages/components/src/Headline/Headline.tsx b/packages/components/src/Headline/Headline.tsx index c21ff57aa0..615637693d 100644 --- a/packages/components/src/Headline/Headline.tsx +++ b/packages/components/src/Headline/Headline.tsx @@ -5,7 +5,7 @@ import { TextAlignProp, cn, createVar, - get, + getColor, textAlign, useClassNames, useTheme, @@ -43,10 +43,7 @@ const _Headline = ({ {...props} className={cn(classNames, 'text-[--color]', textAlign[align])} style={createVar({ - color: - color && - theme.colors && - get(theme.colors, color.replace('-', '.'), color /* fallback */), + color: color && getColor(theme, color, color /* fallback */), })} > {children} diff --git a/packages/components/src/Text/Text.tsx b/packages/components/src/Text/Text.tsx index 536f9ea565..37a89f28de 100644 --- a/packages/components/src/Text/Text.tsx +++ b/packages/components/src/Text/Text.tsx @@ -8,7 +8,7 @@ import { createVar, cursorStyle, fontWeight, - get, + getColor, textAlign, textSize, textStyle, @@ -66,10 +66,7 @@ export const Text = ({ fontSize && textSize[fontSize] )} style={createVar({ - color: - color && - theme.colors && - get(theme.colors, color.replace('-', '.'), color /* fallback */), + color: color && getColor(theme, color, color /* fallback */), })} > {children} diff --git a/packages/system/src/components/SVG/SVG.stories.tsx b/packages/system/src/components/SVG/SVG.stories.tsx index c1100a1ee5..507b6cf96b 100644 --- a/packages/system/src/components/SVG/SVG.stories.tsx +++ b/packages/system/src/components/SVG/SVG.stories.tsx @@ -29,6 +29,16 @@ const meta = { }, }, }, + color: { + control: { + type: 'text', + }, + table: { + defaultValue: { + summary: undefined, + }, + }, + }, }, } satisfies Meta; diff --git a/packages/system/src/components/SVG/SVG.test.tsx b/packages/system/src/components/SVG/SVG.test.tsx index eba034b283..b99df46ba3 100644 --- a/packages/system/src/components/SVG/SVG.test.tsx +++ b/packages/system/src/components/SVG/SVG.test.tsx @@ -32,17 +32,17 @@ test('supports classNames', () => { const svg = screen.getByTestId(/svg/); expect(svg).toMatchInlineSnapshot(` - - - - `); + + + +`); }); test('supports default size', () => { @@ -90,17 +90,17 @@ test('supports responsive sizing', () => { const svg = screen.getByTestId(/svg/); expect(svg).toMatchInlineSnapshot(` - - - - `); + + + +`); }); test('supports custom width instead of default size', () => { @@ -148,3 +148,16 @@ test('forwards ref', () => { expect(ref.current).toBeInstanceOf(SVGElement); }); + +test('supports color prop', () => { + render( + + + + + + ); + const svg = screen.getByTestId(/svg/); + + expect(svg.style.cssText).toMatchInlineSnapshot(`"--color: #ffa8a8;"`); +}); diff --git a/packages/system/src/components/SVG/SVG.tsx b/packages/system/src/components/SVG/SVG.tsx index 8d12924524..236432cb5e 100644 --- a/packages/system/src/components/SVG/SVG.tsx +++ b/packages/system/src/components/SVG/SVG.tsx @@ -2,23 +2,31 @@ import React, { forwardRef } from 'react'; import { HtmlProps } from '@marigold/types'; -import { cn } from '../../utils'; +import { useTheme } from '../../hooks'; +import { cn, createVar, getColor } from '../../utils'; -export interface SVGProps extends Omit, 'fill'> { +export interface SVGProps extends Omit, 'fill' | 'style'> { size?: number | string | number[] | string[]; className?: string; } export const SVG = forwardRef( - ({ size = 24, children, className, ...props }, ref) => ( - - {children} - - ) + ({ size = 24, children, className, color, ...props }, ref) => { + const theme = useTheme(); + + return ( + + {children} + + ); + } ); diff --git a/packages/system/src/utils.test.ts b/packages/system/src/utils.test.ts index c620ff2108..2ffb62760b 100644 --- a/packages/system/src/utils.test.ts +++ b/packages/system/src/utils.test.ts @@ -1,4 +1,4 @@ -import { cva } from './utils'; +import { cva, get, getColor } from './utils'; test('cva (simple)', () => { expect(cva(['text-sm'])()).toMatchInlineSnapshot(`"text-sm"`); @@ -22,3 +22,65 @@ test('cva (variants)', () => { ); expect(styles({ size: 'large' })).toMatchInlineSnapshot(`"text-lg"`); }); + +test('get', () => { + const obj = { + root: 'root-value', + nested: { + value: { + very: { + deep: 'deeeeply-nested-value', + }, + DEFAULT: 'this-is-just-for-reference', + }, + }, + }; + + expect(get(obj, 'does.not.exist')).toMatchInlineSnapshot(`undefined`); + expect(get(obj, 'does.not.exist', 'fallback')).toMatchInlineSnapshot( + `"fallback"` + ); + + expect(get(obj, 'root')).toMatchInlineSnapshot(`"root-value"`); + expect(get(obj, 'nested.value.very.deep')).toMatchInlineSnapshot( + `"deeeeply-nested-value"` + ); + + expect(get(obj, 'nested.value')).toMatchInlineSnapshot(` +{ + "DEFAULT": "this-is-just-for-reference", + "very": { + "deep": "deeeeply-nested-value", + }, +} +`); +}); + +test('getColor', () => { + const theme = { + colors: { + brand: { + 100: 'brand-color', + }, + accent: { + DEFAULT: 'default-accent-color', + hover: 'accent-hover-color', + }, + }, + }; + + expect(getColor(theme, 'does-not-exist')).toMatchInlineSnapshot(`undefined`); + expect(getColor(theme, 'does-not-exist', 'fallback')).toMatchInlineSnapshot( + `"fallback"` + ); + + expect(getColor(theme, 'brand-100')).toMatchInlineSnapshot(`"brand-color"`); + expect(getColor(theme, 'accent-hover')).toMatchInlineSnapshot( + `"accent-hover-color"` + ); + + // Support Tailwinds DEFAULT + expect(getColor(theme, 'accent')).toMatchInlineSnapshot( + `"default-accent-color"` + ); +}); diff --git a/packages/system/src/utils.ts b/packages/system/src/utils.ts index 076a09b9fe..a711ea5787 100644 --- a/packages/system/src/utils.ts +++ b/packages/system/src/utils.ts @@ -59,6 +59,9 @@ export const createVar = (o: { [key: string]: string | number | undefined }) => Object.entries(o).map(([name, val]) => [`--${name}`, val]) ) as React.CSSProperties; +export const isObject = (val: any): val is { [key: string]: any } => + val && val.constructor === Object; + /** * Safely get a dot-notated path within a nested object, with ability * to return a default if the full key path does not exist or @@ -77,3 +80,18 @@ export const get = (obj: object, path: string, fallback?: any): any => { return result === undefined ? fallback : result; }; + +/** + * Safely get a color value from a Tailwind theme object. This also supports + * Tailwind's "DEFAULT" fallback. + * + * Note: Use the CSS "var name" (e.g. primary-500) not the dot notation. + */ +export const getColor = ( + theme: { colors?: object }, + path: string, + fallback?: any +): any => { + const result = get(theme.colors || {}, path.replace('-', '.'), fallback); + return isObject(result) ? result['DEFAULT'] : result; +};