diff --git a/package.json b/package.json index 75287d6af6..3d88d5312f 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "test": "cross-env ts-node --transpile-only ./scripts/test packages", "test:ci": "CI=true yarn test", "bootstrap": "make bootstrap", - "build": "ts-node --transpile-only scripts/build-all", + "build": "ts-node --transpile-only scripts/build-all && yarn build:styles", "build-package": "ts-node --transpile-only scripts/build-package", + "build:styles": "cross-env EXTRACT_CSS=true ts-node ./scripts/extract-styles packages/react", "clean": "make clean", "new-component": "cross-env CI=true ts-node ./scripts/new-component", "nc": "yarn new-component", @@ -102,6 +103,7 @@ "jest": "^26.2.2", "jest-axe": "^3.5.0", "jest-watch-typeahead": "^0.6.0", + "json-to-css": "^0.1.0", "lerna": "^3.22.1", "lerna-script": "^1.3.2", "lodash": "^4.17.19", diff --git a/packages/core/css/package.json b/packages/core/css/package.json deleted file mode 100644 index 8636483ce1..0000000000 --- a/packages/core/css/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@interop-ui/css", - "version": "1.0.0", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/css.esm.min.js", - "types": "dist/types/index.d.ts", - "files": [ - "src", - "dist" - ], - "scripts": { - "test": "cross-env CI=true ts-node --transpile-only ../../../scripts/test --passWithNoTests", - "test:watch": "npm run test -- --watchAll", - "build": "node ../../../scripts/build", - "clean": "ts-node ../../../scripts/clean", - "prepublish": "npm run build" - }, - "dependencies": { - "stylis": "^4.0.2", - "tslib": "^2.0.0" - } -} diff --git a/packages/core/css/src/hashStringIntoClass.ts b/packages/core/css/src/hashStringIntoClass.ts deleted file mode 100644 index 366bcb8869..0000000000 --- a/packages/core/css/src/hashStringIntoClass.ts +++ /dev/null @@ -1,23 +0,0 @@ -import murmurhash from './murmurhash'; -const charsLength = 52; - -/* start at 75 for 'a' until 'z' (25) and then start at 65 for capitalised letters */ -const getAlphabeticChar = (code: number) => String.fromCharCode(code + (code > 25 ? 39 : 97)); - -/* input a number, usually a hash and convert it to base-52 */ -const getClassFromHash = (code: number) => { - let name = ''; - let x; - - /* get a char and divide by alphabet-length */ - for (x = code; x > charsLength; x = Math.floor(x / charsLength)) { - name = getAlphabeticChar(x % charsLength) + name; - } - - return getAlphabeticChar(x % charsLength) + name; -}; - -export const hashCSSStringIntoClass = (cssString: string) => - getClassFromHash(murmurhash(cssString)); - -export default hashCSSStringIntoClass; diff --git a/packages/core/css/src/index.ts b/packages/core/css/src/index.ts deleted file mode 100644 index fc9b11c8b2..0000000000 --- a/packages/core/css/src/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import stringifyCSSObj from './stringifyCSSObj'; -import hashStringIntoClass from './hashStringIntoClass'; -import { Middleware } from './stringifyCSSObj'; -import stylis from './stylis'; - -const cachedClasses = new Set(); - -/** - * Takes a nested css object or a css string and returns a class name that has the stylis applied - * to it - */ -export function css(cssObjOrString: any | string, scope = '', middleware?: Middleware[]) { - // convert the object into a string: - const cssString = stringifyCSSObj(cssObjOrString, middleware || []); - console.log('generating styles:'); - console.log(cssString); - - const styledClass = hashStringIntoClass(cssString); - if (cachedClasses.has(styledClass)) { - return styledClass; - } - // parse and add the stylis to the dom - stylis(scope + '.' + styledClass, cssString); - - cachedClasses.add(styledClass); - - return styledClass; -} - -export type { Middleware }; - -export default css; diff --git a/packages/core/css/src/murmurhash.ts b/packages/core/css/src/murmurhash.ts deleted file mode 100644 index 3060722391..0000000000 --- a/packages/core/css/src/murmurhash.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable no-fallthrough */ -// Source: https://github.com/garycourt/murmurhash-js/blob/master/murmurhash2_gc.js -export function murmurhash(str: string) { - var l = str.length | 0, - h = l | 0, - i = 0, - k; - - while (l >= 4) { - k = - (str.charCodeAt(i) & 0xff) | - ((str.charCodeAt(++i) & 0xff) << 8) | - ((str.charCodeAt(++i) & 0xff) << 16) | - ((str.charCodeAt(++i) & 0xff) << 24); - - k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16); - k ^= k >>> 24; - k = (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16); - - h = ((h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k; - - l -= 4; - ++i; - } - - switch (l) { - case 3: - h ^= (str.charCodeAt(i + 2) & 0xff) << 16; - case 2: - h ^= (str.charCodeAt(i + 1) & 0xff) << 8; - case 1: - h ^= str.charCodeAt(i) & 0xff; - h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16); - } - - h ^= h >>> 13; - h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16); - h ^= h >>> 15; - - return h >>> 0; -} - -export default murmurhash; diff --git a/packages/core/css/src/stringifyCSSObj.ts b/packages/core/css/src/stringifyCSSObj.ts deleted file mode 100644 index 0caeb3ff78..0000000000 --- a/packages/core/css/src/stringifyCSSObj.ts +++ /dev/null @@ -1,39 +0,0 @@ -export type Middleware = (key: string, value: any) => [string, any] | undefined; - -export function stringifyCSSObj(obj: any, middlewareArr: Middleware[] = []) { - let string = ''; - const keys = Object.keys(obj); - for (let index = 0; index < keys.length; index++) { - let key = keys[index]; - let cssFriendlyKey = key.replace(/([A-Z])/g, (matches) => `-${matches[0].toLowerCase()}`); - let value = obj[key]; - - // Apply middleware before we handle the value & key: - for (let m = 0; m < middlewareArr.length; m++) { - const middleware = middlewareArr[m]; - const newKeyAndValue = middleware(cssFriendlyKey, value); - if (newKeyAndValue) { - key = newKeyAndValue[0]; - value = newKeyAndValue[1]; - } - } - - switch (typeof value) { - case 'object': { - string += `${cssFriendlyKey} {${stringifyCSSObj(value, middlewareArr)}}`; - continue; - } - case 'number': - case 'string': { - string += `${cssFriendlyKey}:${value};`; - continue; - } - default: { - console.error(`Unsupported property value [${typeof value}]`); - } - } - } - return string; -} - -export default stringifyCSSObj; diff --git a/packages/core/css/src/stylis.ts b/packages/core/css/src/stylis.ts deleted file mode 100644 index 8e7ae6f031..0000000000 --- a/packages/core/css/src/stylis.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { compile, serialize, rulesheet, middleware, stringify } from 'stylis'; - -let interopStyleElm: HTMLStyleElement | null = document.querySelector('style[data-interop-styles]'); - -let sheet: undefined | CSSStyleSheet; - -if (interopStyleElm) { - sheet = interopStyleElm.sheet as CSSStyleSheet; -} - -if (!interopStyleElm || !sheet) { - interopStyleElm = document.createElement('style'); - interopStyleElm.setAttribute('data-interop-styles', 'true'); - sheet = document.head.appendChild(interopStyleElm).sheet as CSSStyleSheet; -} - -if (!sheet) { - throw new Error('something is wrong yo!'); -} - -let cssSheetLength = sheet.cssRules.length; - -// stylis -export const stylis = (scope: string, cssString: string) => { - // parse the css string using stylis and add it to the sheet - serialize( - compile(`${scope}{${cssString}}`), - middleware([ - stringify, - rulesheet((value: any) => { - cssSheetLength = sheet!.insertRule(value, cssSheetLength) + 1; - }), - ]) - ); -}; - -export default stylis; diff --git a/packages/core/utils/src/domUtils.ts b/packages/core/utils/src/domUtils.ts index 318ee882fa..733ac19b01 100644 --- a/packages/core/utils/src/domUtils.ts +++ b/packages/core/utils/src/domUtils.ts @@ -2,13 +2,23 @@ import kebabCase from 'lodash.kebabcase'; import { isFunction } from './typeUtils'; export function interopDataAttr(componentPart: string) { - return `data-interop-part-${kebabCase(componentPart)}`; + return `data-interop-${kebabCase(componentPart)}`; } export function interopDataAttrObj(componentPart: string) { return { [interopDataAttr(componentPart)]: '' }; } +export function interopSelector(componentPart: string) { + return process.env.EXTRACT_CSS + ? interopDataAttrSelector(componentPart) + : componentPart.toLowerCase().split('.').reverse()[0]; +} + +export function interopDataAttrSelector(componentPart: string) { + return `[${interopDataAttr(componentPart)}]`; +} + /** * Get an element's owner document. Useful when components are used in iframes * or other environments like dev tools. diff --git a/packages/react/accordion/src/Accordion.tsx b/packages/react/accordion/src/Accordion.tsx index 2578914bb2..c6655dda14 100644 --- a/packages/react/accordion/src/Accordion.tsx +++ b/packages/react/accordion/src/Accordion.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { cssReset, interopDataAttrObj } from '@interop-ui/utils'; +import { cssReset, interopDataAttrObj, interopSelector } from '@interop-ui/utils'; import { composeEventHandlers, createContext, @@ -38,6 +38,7 @@ const [AccordionContext, useAccordionContext] = createContext; @@ -53,7 +54,7 @@ type AccordionItemContextValue = { const [AccordionItemContext, useAccordionItemContext] = createContext( 'AccordionItemContext', - 'Accordion.Item' + ITEM_NAME ); const AccordionItem = forwardRef( @@ -66,7 +67,7 @@ const AccordionItem = forwardRef( onToggle, ...accordionItemProps } = props; - const accordionContext = useAccordionContext('Accordion.Item'); + const accordionContext = useAccordionContext(ITEM_NAME); const generatedButtonId = `accordion-button-${useId()}`; const buttonId = props.id || generatedButtonId; @@ -80,7 +81,7 @@ const AccordionItem = forwardRef( return ( ( } ); -AccordionItem.displayName = 'Accordion.Item'; - /* ------------------------------------------------------------------------------------------------- * AccordionHeader * -----------------------------------------------------------------------------------------------*/ +const HEADER_NAME = 'Accordion.Header'; const HEADER_DEFAULT_TAG = 'h3'; type AccordionHeaderDOMProps = React.ComponentPropsWithoutRef; @@ -110,16 +110,15 @@ const AccordionHeader = forwardRef; + return ; } ); -AccordionHeader.displayName = 'Accordion.Header'; - /* ------------------------------------------------------------------------------------------------- * AccordionButton * -----------------------------------------------------------------------------------------------*/ +const BUTTON_NAME = 'Accordion.Button'; const BUTTON_DEFAULT_TAG = 'button'; type AccordionButtonDOMProps = React.ComponentPropsWithoutRef; @@ -131,8 +130,8 @@ type AccordionButtonProps = CollapsibleButtonProps & const AccordionButton = forwardRef( function AccordionButton(props, forwardedRef) { const { ...buttonProps } = props; - const { buttonNodesRef } = useAccordionContext('Accordion.Header'); - const itemContext = useAccordionItemContext('Accordion.Header'); + const { buttonNodesRef } = useAccordionContext(BUTTON_NAME); + const itemContext = useAccordionItemContext(BUTTON_NAME); const ref = React.useRef | null>(null); const composedRefs = useComposedRefs(ref, forwardedRef); @@ -153,7 +152,7 @@ const AccordionButton = forwardRef; @@ -205,11 +202,11 @@ type AccordionPanelProps = CollapsibleContentProps & const AccordionPanel = forwardRef( function AccordionPanel(props, forwardedRef) { - const itemContext = useAccordionItemContext('Accordion.Panel'); + const itemContext = useAccordionItemContext(PANEL_NAME); return ( } ); -AccordionPanel.displayName = 'Accordion.Panel'; - /* ------------------------------------------------------------------------------------------------- * Accordion * -----------------------------------------------------------------------------------------------*/ +const ACCORDION_NAME = 'Accordion'; const ACCORDION_DEFAULT_TAG = 'div'; type AccordionDOMProps = Omit< @@ -311,7 +307,7 @@ const Accordion = forwardRef @@ -321,8 +317,6 @@ const Accordion = forwardRef; @@ -39,11 +40,11 @@ type CollapsibleButtonProps = CollapsibleButtonDOMProps & CollapsibleButtonOwnPr const CollapsibleButton = forwardRef( (props, forwardedRef) => { let { as: Comp = BUTTON_DEFAULT_TAG, onClick, ...buttonProps } = props; - let context = useCollapsibleContext('Collapsible.Button'); + let context = useCollapsibleContext(BUTTON_NAME); return ( ; @@ -70,7 +70,7 @@ type CollapsibleContentProps = CollapsibleContentOwnProps & CollapsibleContentDO const CollapsibleContent = forwardRef( (props, forwardedRef) => { const { as: Comp = CONTENT_DEFAULT_TAG, id: idProp, children, ...contentProps } = props; - const { setContentId, isOpen } = useCollapsibleContext('Collapsible.Content'); + const { setContentId, isOpen } = useCollapsibleContext(CONTENT_NAME); const generatedId = `collapsible-${useId()}`; const id = idProp || generatedId; @@ -80,7 +80,7 @@ const CollapsibleContent = forwardRef + {children} ); @@ -160,23 +159,26 @@ const Collapsible = forwardRef< Collapsible.Button = CollapsibleButton; Collapsible.Content = CollapsibleContent; -Collapsible.displayName = 'Collapsible'; +Collapsible.displayName = COLLAPSIBLE_NAME; +Collapsible.Button.displayName = BUTTON_NAME; +Collapsible.Content.displayName = CONTENT_NAME; const styles: PrimitiveStyles = { - collapsible: { + [interopSelector(COLLAPSIBLE_NAME)]: { ...cssReset(COLLAPSIBLE_DEFAULT_TAG), }, - button: { + [interopSelector(BUTTON_NAME)]: { ...cssReset(BUTTON_DEFAULT_TAG), display: 'block', width: '100%', textAlign: 'inherit', userSelect: 'none', + + '&:disabled': { + pointerEvents: 'none', + }, }, - 'collapsible.state.disabled[button]': { - pointerEvents: 'none', - }, - content: { + [interopSelector(CONTENT_NAME)]: { ...cssReset(CONTENT_DEFAULT_TAG), }, }; diff --git a/packages/react/style/package.json b/packages/react/style/package.json deleted file mode 100644 index cac1120ec9..0000000000 --- a/packages/react/style/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@interop-ui/react-style", - "version": "1.0.0", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/react-style.esm.min.js", - "types": "dist/types/index.d.ts", - "files": [ - "src", - "dist" - ], - "scripts": { - "test": "cross-env CI=true ts-node --transpile-only ../../../scripts/test --passWithNoTests", - "test:watch": "npm run test -- --watchAll", - "build": "node ../../../scripts/build", - "clean": "ts-node ../../../scripts/clean", - "prepublish": "npm run build" - }, - "dependencies": { - "@interop-ui/css": "1.0.0", - "@interop-ui/react-utils": "1.0.0", - "@interop-ui/utils": "1.0.0", - "hoist-non-react-statics": "^3.3.2", - "lodash.merge": "^4.6.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0" - } -} diff --git a/packages/react/style/src/InteropProvider.tsx b/packages/react/style/src/InteropProvider.tsx deleted file mode 100644 index a5294630e5..0000000000 --- a/packages/react/style/src/InteropProvider.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { ThemeManager } from './Theme'; - -const defaultBreakPoints = ThemeManager.theme.breakpoints; - -export const ResponsiveContext = React.createContext([]); - -export function InteropProvider(props: any) { - const [breakPointsMatch, setBreakPointMatch] = React.useState([]); - - React.useLayoutEffect(() => { - ThemeManager.injectTheme(); - const queries = defaultBreakPoints.map((breakPoint) => - window.matchMedia(`(min-width:${breakPoint})`) - ); - // set initial matches - setBreakPointMatch(queries.map((match) => match.matches)); - - // monitor matches for changes - queries.forEach((query) => { - query.addListener((_) => { - setBreakPointMatch(queries.map((match) => match.matches)); - }); - }); - }, []); - - return ; -} - -export const _matchValuesAgainstBreakPoints = ( - valueOrValues: string | string[], - breakpoints: Boolean[] -) => { - // not an array === just return it - if (!(valueOrValues instanceof Array)) { - return valueOrValues; - } - // first value is always a match - const [firstValue, ...Rest] = valueOrValues; - - let matchedValue = firstValue; - for (let i = 0; i < breakpoints.length; i++) { - const isAMatch = breakpoints[i]; - // media query is a match - if (isAMatch) { - // we have a value with that index - if (Rest[i]) { - // update the matched value - matchedValue = Rest[i]; - } - } else { - // query does not match - return matchedValue; - } - } - return matchedValue; -}; - -export const useResponsiveValue = (valueOrMultipleValues: string | string[]) => { - const matches = React.useContext(ResponsiveContext); - return _matchValuesAgainstBreakPoints(valueOrMultipleValues, matches); -}; diff --git a/packages/react/style/src/Theme.tsx b/packages/react/style/src/Theme.tsx deleted file mode 100644 index 5c277d6553..0000000000 --- a/packages/react/style/src/Theme.tsx +++ /dev/null @@ -1,95 +0,0 @@ -export const ThemeManager = { - theme: { - breakpoints: ['600px', '1000px', '1080px', '1760px'], - fonts: { - normal: - 'UntitledSans, -apple-system, BlinkMacSystemFont, "Helvetica Neue", helvetica, arial, sans-serif', - mono: 'RadixDuo, "Liberation Mono", Menlo, Consolas, monospace', - }, - fontSizes: [ - '10px', - '12px', - '13px', - '15px', - '17px', - '19px', - '21px', - '23px', - '27px', - '35px', - '58px', - ], - space: ['0', '5px', '10px', '15px', '20px', '25px', '35px', '45px', '65px', '80px'], - sizes: ['0', '5px', '10px', '15px', '20px', '25px', '35px', '45px', '65px', '80px'], - lineHeights: ['15px', '20px', '25px', '30px', '35px', '40px', '45px', '50px', '55px', '60px'], - radii: ['0', '3px', '5px', '10px'], - colors: { - black: 'hsl(0, 0%, 0%)', - white: 'hsl(0, 0%, 100%)', - - gray100: 'hsl(210, 10%, 99%)', - gray200: 'hsl(210, 25%, 95%)', - gray300: 'hsl(210, 15%, 90%)', - gray400: 'hsl(210, 10%, 85%)', - gray500: 'hsl(210, 10%, 75%)', - gray600: 'hsl(210, 8%, 62%)', - gray700: 'hsl(210, 7%, 43%)', - gray800: 'hsl(210, 7%, 17%)', - gray900: 'hsl(210, 5%, 9%)', - - blue100: 'hsl(208, 100%, 98%)', - blue200: 'hsl(208, 100%, 95%)', - blue300: 'hsl(208, 95%, 90%)', - blue400: 'hsl(208, 94%, 81%)', - blue500: 'hsl(208, 95%, 68%)', - blue600: 'hsl(208, 98%, 50%)', - blue700: 'hsl(208, 99%, 44%)', - blue800: 'hsl(208, 98%, 14%)', - blue900: 'hsl(208, 98%, 9%)', - - red100: 'hsl(348, 100%, 98%)', - red200: 'hsl(356, 92%, 96%)', - red300: 'hsl(357, 87%, 91%)', - red400: 'hsl(358, 90%, 85%)', - red500: 'hsl(358, 92%, 74%)', - red600: 'hsl(350, 95%, 52%)', - red700: 'hsl(348, 97%, 45%)', - red800: 'hsl(345, 100%, 20%)', - red900: 'hsl(338, 100%, 12%)', - - green100: 'hsl(150, 80%, 98%)', - green200: 'hsl(143, 64%, 94%)', - green300: 'hsl(144, 60%, 86%)', - green400: 'hsl(145, 59%, 78%)', - green500: 'hsl(148, 53%, 60%)', - green600: 'hsl(148, 60%, 42%)', - green700: 'hsl(150, 70%, 30%)', - green800: 'hsl(149, 63%, 15%)', - green900: 'hsl(144, 61%, 8%)', - - yellow100: 'hsl(42, 100%, 98%)', - yellow200: 'hsl(42, 94%, 93%)', - yellow300: 'hsl(45, 89%, 86%)', - yellow400: 'hsl(50, 92%, 74%)', - yellow500: 'hsl(51, 94%, 66%)', - yellow600: 'hsl(52, 100%, 49%)', - yellow700: 'hsl(35, 50%, 39%)', - yellow800: 'hsl(35, 50%, 15%)', - yellow900: 'hsl(32, 50%, 8%)', - }, - }, - setTheme(theme: any) { - this.theme = theme; - }, - // TODO: type This here to make this work without casting - injectTheme() { - const scales = Object.keys(this.theme); - for (let i = 0; i < scales.length; i++) { - const scaleKey = scales[i]; - const scaleValue = (this.theme as any)[scaleKey]; - Object.entries(scaleValue).forEach(([key, value]) => { - document.body.style.setProperty(`--mdz-${scaleKey}-${key}`, value as string); - }); - } - }, -}; diff --git a/packages/react/style/src/index.ts b/packages/react/style/src/index.ts deleted file mode 100644 index 54fcd8052f..0000000000 --- a/packages/react/style/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './style'; diff --git a/packages/react/style/src/style.tsx b/packages/react/style/src/style.tsx deleted file mode 100644 index 02242aa2c3..0000000000 --- a/packages/react/style/src/style.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import * as React from 'react'; -import * as CSS from 'csstype'; -import hoist from 'hoist-non-react-statics'; -import { css } from '@interop-ui/css'; -import merge from 'lodash.merge'; -import { ResponsiveContext, _matchValuesAgainstBreakPoints } from './InteropProvider'; -import { themeValuesMiddleware } from './themeValuesMiddleware'; -import { FunctionComponentWithAs, ExoticComponentWithAs, PropsFromAs } from '../../utils/src/types'; - -let currentCount = 0; -const cachedClasskey = Symbol('variant cached class property key'); - -function useResolvePropsIntoVariantClasses( - props: any, - internalVariants: InternalVariants, - scope: string -) { - const variantKeys = Object.keys(internalVariants); - const breakPointMatches = React.useContext(ResponsiveContext); - const classes = []; - // iterate over variant keys. ie: color, size, variant - for (let i = 0; i < variantKeys.length; i++) { - const variantKey = variantKeys[i]; - // match the variant key in the props - if (props[variantKey]) { - const variantValueInProps: string | string[] = props[variantKey]; - // match the value in props to media queries - const resolvedResponsiveVariantsIntoOne = _matchValuesAgainstBreakPoints( - variantValueInProps, - breakPointMatches - ); - - // match in internal variants? - const variantStyles = internalVariants[variantKey][resolvedResponsiveVariantsIntoOne]; - - // does the resolved variant have a match inside our internal variants? - if (variantStyles) { - // did we parse this variant before? - if (variantStyles[cachedClasskey]) { - classes.push(variantStyles[cachedClasskey]); - } else { - // we've never seen this variant before - const variantClass = css(variantStyles, scope, [themeValuesMiddleware]); - classes.push(variantClass); - variantStyles[cachedClasskey] = variantClass; - } - // empty the variant in the prop - delete props[variantKey]; - } - } - } - - return classes.join(' '); -} - -/** Main export */ -function style(tag: ComponentType) { - const _styles = {}; - const _variants: InternalVariants = {}; - const elmId = requestElmId(); - let _styledClass: string | undefined = undefined; - - type Props = ComponentType extends FunctionComponentWithAs - ? PropsFromAs - : ComponentType extends ExoticComponentWithAs - ? PropsFromAs - : ComponentType extends JSXTags - ? any // React.ComponentPropsWithRef TODO: Not sure why this causes problems - : any; - - function InteropElm(props: Props & VariantsAndValues & { className?: string }, ref: any): any; - - function InteropElm( - props: Props & VariantsAndValues & { className?: string }, - // TODO: type me - ref: any - ) { - let styledClass = _styledClass; - if (!styledClass) { - styledClass = css(_styles); - _styledClass = styledClass; - } - - const mutableProps = { ...props }; - const variantClasses = useResolvePropsIntoVariantClasses( - mutableProps, - _variants, - '.' + styledClass - ); - - return React.createElement(props.as || tag, { - ...mutableProps, - className: [props.className, elmId, styledClass, variantClasses].join(' ').trim(), - }); - } - - /** - * Style static method: - */ - InteropElm.style = (styles: CSSDeclaration) => { - merge(_styles, styles); - return InteropElm; - }; - - /** - * Variant static method: - */ - InteropElm.variant = (name: string, styles: { [p: string]: CSSDeclaration }) => { - _variants[name] = _variants[name] || {}; - const keys = Object.keys(styles); - for (let index = 0; index < keys.length; index++) { - const currentKey = keys[index]; - _variants[name][currentKey] = _variants[name][currentKey] || {}; - merge(_variants[name][currentKey], styles[currentKey]); - } - - return InteropElm; - }; - - // id for referencing - InteropElm.toString = () => '.' + elmId; - - // internal function used to handle composing this element's - // styles into others - // this is not typed externally on purpose as it's intended to be - // used by the library internally - InteropElm._compose = (variants: VariantsAndValues) => { - const composedStyles = Object.assign({}, _styles); - const variantKeys = Object.keys(variants); - for (let i = 0; i < variantKeys.length; i++) { - const variantKey = variantKeys[i]; - const variantValue = variants[variantKey]; - if (variantValue && _variants[variantKey] && _variants[variantKey][variantValue]) { - merge(composedStyles, _variants[variantKey][variantValue]); - } - } - return composedStyles; - }; - - // forcing the type because we're hoisting below - const forwardedRef = (React.forwardRef(InteropElm as any) as any) as BaseInterop< - any, - VariantsAndValues - >; - - // Hoist our static properties to the top. - // We need to do this because we're forwarding refs above, and because the `tag` might be a React - // component with its own static properties. - if (tag && typeof tag !== 'string') { - hoist(InteropElm, tag as any); - } - hoist(forwardedRef, InteropElm); - - return forwardedRef; -} - -/** - * types: - */ -type ExtractVariants = InteropElm extends BaseInterop ? B : {}; - -interface NestedCSSDeclarations { - [name: string]: CSSDeclaration; -} - -type CSSDeclaration = { [P in keyof CSS.Properties]?: CSS.Properties[P] } | NestedCSSDeclarations; - -type JSXTags = keyof JSX.IntrinsicElements; - -type ResponsiveVariants = { - [p in keyof VariantsAndValues]: VariantsAndValues[p] | Exclude[]; -}; - -interface BaseInterop { - ( - props: React.ComponentPropsWithRef & - ResponsiveVariants & { - as?: never; - className?: string; - }, - ref: any - ): any; - ( - props: React.ComponentPropsWithRef & - ResponsiveVariants & { as?: AS; className?: string }, - ref: any - ): any; - styledClass: string; - style: (styles: CSSDeclaration) => BaseInterop; - variant: ( - name: V, - styles: { [P in Exclude]: CSSDeclaration } - ) => BaseInterop; -} - -/** utils: */ -function compose>(part: T, variants: ExtractVariants) { - // we dont want to expose _compose externally so we're casting - // it when working with library internals - const internallyTyped = (part as any) as T & { - _compose: (variants: ExtractVariants) => CSSDeclaration; - }; - - return internallyTyped._compose(variants); -} - -type IVariantsAndValue = { - [p: string]: string | undefined; -}; - -type InternalVariants = { - [p: string]: { - [p: string]: CSSDeclaration & { [cachedClasskey]?: string }; - }; -}; - -export { style, compose }; -export default style; - -function requestElmId() { - const elmId = `intrp_${currentCount}`; - currentCount = currentCount + 1; - return elmId; -} diff --git a/packages/react/style/src/themeValuesMiddleware.tsx b/packages/react/style/src/themeValuesMiddleware.tsx deleted file mode 100644 index 8ae363958a..0000000000 --- a/packages/react/style/src/themeValuesMiddleware.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Middleware } from '@interop-ui/css'; - -// map values to theme values -const colorScaleKeys = new Set(['color', 'background-color', 'background', 'border-color']); - -const themeValuePrefix = '$'; -export const themeValuesMiddleware: Middleware = function (key, value) { - if (value[0] === themeValuePrefix) { - if (colorScaleKeys.has(key)) { - return [key, `var(--mdz-colors-${value.substr(1)})`]; - } - } - return; -}; diff --git a/packages/react/utils/src/forwardRef.tsx b/packages/react/utils/src/forwardRef.tsx index c1e3347403..b0da187300 100644 --- a/packages/react/utils/src/forwardRef.tsx +++ b/packages/react/utils/src/forwardRef.tsx @@ -9,6 +9,6 @@ export function forwardRef ) { type ComponentWithStaticProps = StaticProps & - ForwardRefExoticComponentWithAs; + ForwardRefExoticComponentWithAs & { displayName: string }; return React.forwardRef(render) as ComponentWithStaticProps; } diff --git a/packages/react/utils/src/types.tsx b/packages/react/utils/src/types.tsx index c0407cc326..c11956b620 100644 --- a/packages/react/utils/src/types.tsx +++ b/packages/react/utils/src/types.tsx @@ -111,5 +111,5 @@ export interface ForwardRefWithAsRenderFunction = ElementTagNameMap[TagName]; export type PrimitiveStyles = { - [part: string]: React.CSSProperties | null; + [part: string]: React.CSSProperties | null | Record; }; diff --git a/scripts/extract-styles.ts b/scripts/extract-styles.ts new file mode 100644 index 0000000000..64b0919f70 --- /dev/null +++ b/scripts/extract-styles.ts @@ -0,0 +1,58 @@ +import fs from 'fs'; +import path from 'path'; +import css from 'json-to-css'; + +const projectRoot = path.resolve(__dirname, '../'); +const dirPath = projectRoot + '/' + process.argv[2]; + +function build() { + const files = fs.readdirSync(dirPath); + + files.forEach(function (file) { + const source = dirPath + '/' + file; + + if (fs.statSync(source).isDirectory()) { + const { styles } = require(source); + + if (styles) { + const cssObj = flattenStyles(styles); + const cssStyles = css.of(cssObj); + const cssFile = source + '/dist/styles.css'; + + fs.writeFile(cssFile, cssStyles, (error) => { + if (error) { + console.log(`Error creating ${cssFile}: `, error); + } else { + console.log('Created file: ', cssFile); + } + }); + } + } + }); +} + +type StyleObject = Record; +type NestedStyleObject = StyleObject; + +function flattenStyles( + nestedStyles: NestedStyleObject, + rootSelector: string[] = [] +): StyleObject> { + return Object.entries(nestedStyles).reduce((acc, [key, value]) => { + if (!value || !Object.values(value).length) return acc; + + const flattened = isNestedObject(value) + ? flattenStyles(value, [...rootSelector, key]) + : { [[...rootSelector, key.replace('&', '')].join('')]: value }; + + return Object.assign({}, acc, flattened); + }, {}); +} + +const isNestedObject = (value: any): value is NestedStyleObject => { + return ( + typeof value === 'object' && Object.entries(value).some(([, v]) => v && typeof v === 'object') + ); +}; + +build(); diff --git a/types/index.d.ts b/types/index.d.ts index 458060b22d..a59622acda 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,6 @@ declare const __DEV__: boolean; -declare module 'stylis'; +declare module 'json-to-css'; declare module 'lodash.isequal' { import { isEqual } from 'lodash'; diff --git a/yarn.lock b/yarn.lock index 97330601cd..528533dccc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8503,7 +8503,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -10036,6 +10036,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json-to-css@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/json-to-css/-/json-to-css-0.1.0.tgz#4b3d104e8ed2c81b8a071eb86953d1936b3a08e6" + integrity sha512-0A6Xooey9slodt9MX7wWRVNo5G7fujVlBkdyhjNPg3GzoJW7j5EBD/Duw3zK3V132Ma/HTEnBRchl1PmOWiZgg== + json3@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"