UNPKG

@vitus-labs/rocketstories

Version:

Rocketstyle is ultra powerful and extensible styling system for building React components blazingly fast, easily and make them easily extensible and reusable.

1,388 lines (1,357 loc) 65.8 kB
import { isEmpty, config, pick, omit, compose, render, context as context$2, merge, set, get, HTML_TAGS } from '@vitus-labs/core'; import React, { createContext, forwardRef, useMemo, useRef, useImperativeHandle, useContext as useContext$1, useState, useCallback, createElement, Fragment } from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; import { Element, List } from '@vitus-labs/elements'; import { styles } from '@vitus-labs/unistyle'; const PSEUDO_KEYS = ['hover', 'active', 'focus', 'pressed']; const PSEUDO_META_KEYS = ['disabled', 'readOnly']; const THEME_MODES = { light: true, dark: true, }; const THEME_MODES_INVERSED = { dark: 'light', light: 'dark', }; const CONFIG_KEYS = [ 'provider', 'consumer', 'DEBUG', 'name', 'component', 'inversed', 'passProps', 'styled', ]; const STYLING_KEYS = ['theme', 'styles']; const STATIC_KEYS = [...STYLING_KEYS, 'compose']; const ALL_RESERVED_KEYS = [ ...Object.keys(THEME_MODES), ...CONFIG_KEYS, ...STATIC_KEYS, 'attrs', ]; const context$1 = createContext({}); const useLocalContext = (consumer) => { const ctx = consumer ? useContext$1(context$1) : {}; const result = consumer ? consumer((callback) => callback(ctx)) : {}; return { pseudo: {}, ...result }; }; const LocalProvider = context$1.Provider; const usePseudoState = ({ onBlur, onFocus, onMouseDown, onMouseEnter, onMouseLeave, onMouseUp, }) => { const [hover, setHover] = useState(false); const [focus, setFocus] = useState(false); const [pressed, setPressed] = useState(false); const handleOnMouseEnter = useCallback((e) => { setHover(true); if (onMouseEnter) onMouseEnter(e); }, [onMouseEnter]); const handleOnMouseLeave = useCallback((e) => { setHover(false); setPressed(false); if (onMouseLeave) onMouseLeave(e); }, [onMouseLeave]); const handleOnMouseDown = useCallback((e) => { setPressed(true); if (onMouseDown) onMouseDown(e); }, [onMouseDown]); const handleOnMouseUp = useCallback((e) => { setPressed(false); if (onMouseUp) onMouseUp(e); }, [onMouseUp]); const handleOnFocus = useCallback((e) => { setFocus(true); if (onFocus) onFocus(e); }, [onFocus]); const handleOnBlur = useCallback((e) => { setFocus(false); if (onBlur) onBlur(e); }, [onBlur]); return { state: { hover, focus, pressed, }, events: { onMouseEnter: handleOnMouseEnter, onMouseLeave: handleOnMouseLeave, onMouseDown: handleOnMouseDown, onMouseUp: handleOnMouseUp, onFocus: handleOnFocus, onBlur: handleOnBlur, }, }; }; const useRocketstyleRef = ({ $rocketstyleRef, ref }) => { const internalRef = useRef(null); useImperativeHandle($rocketstyleRef, () => internalRef.current); useImperativeHandle(ref, () => internalRef.current); return internalRef; }; const useThemeAttrs = ({ inversed }) => { const { theme = {}, mode: ctxMode = 'light', isDark: ctxDark, } = useContext$1(context$2) || {}; const mode = inversed ? THEME_MODES_INVERSED[ctxMode] : ctxMode; const isDark = inversed ? !ctxDark : ctxDark; const isLight = !isDark; return { theme, mode, isDark, isLight }; }; const RocketStyleProviderComponent = (WrappedComponent) => forwardRef(({ onMouseEnter, onMouseLeave, onMouseUp, onMouseDown, onFocus, onBlur, $rocketstate, ...props }, ref) => { // pseudo hook to detect states hover / pressed / focus const pseudo = usePseudoState({ onMouseEnter, onMouseLeave, onMouseUp, onMouseDown, onFocus, onBlur, }); const updatedState = useMemo(() => ({ ...$rocketstate, pseudo: { ...$rocketstate.pseudo, ...pseudo.state }, }), [$rocketstate, pseudo]); return (React.createElement(LocalProvider, { value: updatedState }, React.createElement(WrappedComponent, { ...props, ...pseudo.events, ref: ref, "$rocketstate": updatedState }))); }); class ThemeManager { baseTheme = new WeakMap(); dimensionsThemes = new WeakMap(); modeBaseTheme = { light: new WeakMap(), dark: new WeakMap() }; modeDimensionTheme = { light: new WeakMap(), dark: new WeakMap() }; } /* eslint-disable no-param-reassign */ const removeUndefinedProps = (props) => Object.keys(props).reduce((acc, key) => { const currentValue = props[key]; if (currentValue !== undefined) return { ...acc, [key]: currentValue }; return acc; }, {}); const pickStyledAttrs = (props, keywords) => Object.keys(props).reduce((acc, key) => { if (keywords[key] && props[key]) acc[key] = props[key]; return acc; }, {}); const calculateChainOptions = (options) => (args) => { const result = {}; if (isEmpty(options)) return result; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return options.reduce((acc, item) => Object.assign(acc, item(...args)), {}); }; const calculateStylingAttrs = ({ useBooleans, multiKeys }) => ({ props, dimensions }) => { const result = {}; // (1) find dimension keys values & initialize // object with possible options Object.keys(dimensions).forEach((item) => { const pickedProp = props[item]; const valueTypes = ['number', 'string']; // if the property is multi key, allow assign array as well if (multiKeys && multiKeys[item] && Array.isArray(pickedProp)) { result[item] = pickedProp; } // assign when it's only a string or number otherwise it's considered // as invalid param else if (valueTypes.includes(typeof pickedProp)) { result[item] = pickedProp; } else { result[item] = undefined; } }); // (2) if booleans are being used let's find the rest if (useBooleans) { const propsKeys = Object.keys(props).reverse(); Object.entries(result).forEach(([key, value]) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const isMultiKey = multiKeys[key]; // when value in result is not assigned yet if (!value) { let newDimensionValue; const keywords = Object.keys(dimensions[key]); if (isMultiKey) { newDimensionValue = propsKeys.filter((key) => keywords.includes(key)); } else { // reverse props to guarantee the last one will have // a priority over previous ones newDimensionValue = propsKeys.find((key) => { if (keywords.includes(key) && props[key]) return key; return false; }); } result[key] = newDimensionValue; } }); } return result; }; /* eslint-disable no-underscore-dangle */ const rocketStyleHOC = ({ inversed, attrs, priorityAttrs }) => { // -------------------------------------------------- // .attrs(...) // first we need to calculate final props which are // being returned by using `attr` chaining method // -------------------------------------------------- const calculateAttrs = calculateChainOptions(attrs); const calculatePriorityAttrs = calculateChainOptions(priorityAttrs); const Enhanced = (WrappedComponent) => forwardRef((props, ref) => { const { theme, mode, isDark, isLight } = useThemeAttrs({ inversed, }); const callbackParams = [theme, { render, mode, isDark, isLight }]; // -------------------------------------------------- // remove undefined props not to override potential default props // only props with value (e.g. `null`) should override default props // -------------------------------------------------- const filteredProps = removeUndefinedProps(props); const prioritizedAttrs = calculatePriorityAttrs([ filteredProps, ...callbackParams, ]); const finalAttrs = calculateAttrs([ { ...prioritizedAttrs, ...filteredProps, }, ...callbackParams, ]); return (React.createElement(WrappedComponent, { "$rocketstyleRef": ref, ...prioritizedAttrs, ...finalAttrs, ...filteredProps })); }); return Enhanced; }; const createStaticsChainingEnhancers = ({ context, dimensionKeys, func, options, }) => { const keys = [...dimensionKeys, ...STATIC_KEYS]; keys.forEach((item) => { // eslint-disable-next-line no-param-reassign context[item] = (props) => func(options, { [item]: props }); }); }; const createStaticsEnhancers = ({ context, options, }) => { if (!isEmpty(options)) { Object.assign(context, options); } }; /* eslint-disable import/prefer-default-export */ const removeNullableValues = (obj) => Object.entries(obj) .filter(([, v]) => v != null && v !== false) .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}); // -------------------------------------------------------- // Remove All Empty Values // -------------------------------------------------------- // type RemoveAllEmptyValues = (obj: Record<string, any>) => Record<string, any> // export const removeAllEmptyValues: RemoveAllEmptyValues = (obj) => // Object.entries(obj) // .filter(([, v]) => v != null) // .reduce( // (acc, [k, v]) => ({ // ...acc, // [k]: typeof v === 'object' ? removeAllEmptyValues(v) : v, // }), // {} // ) const isValidKey = (value) => value !== undefined && value !== null && value !== false; const isMultiKey = (value) => { if (typeof value === 'object' && value !== null) return [true, get(value, 'propName')]; return [false, value]; }; const getDimensionsMap = ({ themes, useBooleans }) => { const result = { keysMap: {}, keywords: {}, }; if (isEmpty(themes)) return result; return Object.entries(themes).reduce((accumulator, [key, value]) => { const { keysMap, keywords } = accumulator; keywords[key] = true; Object.entries(value).forEach(([itemKey, itemValue]) => { if (!isValidKey(itemValue)) return; if (useBooleans) { keywords[itemKey] = true; } set(keysMap, [key, itemKey], true); }); return accumulator; }, result); }; const getKeys = (obj) => Object.keys(obj); const getValues = (obj) => Object.values(obj); const getDimensionsValues = (obj) => getValues(obj).map((item) => { if (typeof item === 'object') { return item.propName; } return item; }); const getMultipleDimensions = (obj) => getValues(obj).reduce((accumulator, value) => { if (typeof value === 'object') { // eslint-disable-next-line no-param-reassign if (value.multi === true) accumulator[value.propName] = true; } return accumulator; }, {}); /* eslint-disable no-param-reassign */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // -------------------------------------------------------- // Theme Mode Callback // -------------------------------------------------------- const themeModeCallback = (light, dark) => (mode) => { if (!mode || mode === 'light') return light; return dark; }; const isModeCallback = (value) => typeof value === 'function' && //@ts-ignore value.toString() === themeModeCallback().toString(); const getThemeFromChain = (options, theme) => { const result = {}; if (!options || isEmpty(options)) return result; return options.reduce((acc, item) => merge(acc, item(theme, themeModeCallback, config.css)), result); }; const getDimensionThemes = (theme, options) => { const result = {}; if (isEmpty(options.dimensions)) return result; return Object.entries(options.dimensions).reduce((acc, [key, value]) => { const [, dimension] = isMultiKey(value); const helper = options[key]; if (Array.isArray(helper) && helper.length > 0) { const finalDimensionThemes = getThemeFromChain(helper, theme); // eslint-disable-next-line no-param-reassign acc[dimension] = removeNullableValues(finalDimensionThemes); } return acc; }, result); }; const getTheme$1 = ({ rocketstate, themes, baseTheme }) => { // generate final theme which will be passed to styled component let finalTheme = { ...baseTheme }; Object.entries(rocketstate).forEach(([key, value]) => { const keyTheme = themes[key]; if (Array.isArray(value)) { value.forEach((item) => { finalTheme = merge({}, finalTheme, keyTheme[item]); }); } else { finalTheme = merge({}, finalTheme, keyTheme[value]); } }); return finalTheme; }; const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) => { const value = object[key]; if (typeof value === 'object' && value !== null) { acc[key] = getThemeByMode(value, mode); } else if (isModeCallback(value)) { acc[key] = value(mode); } else { acc[key] = value; } return acc; }, {}); const chainOptions = (opts, defaultOpts = []) => { const result = [...defaultOpts]; if (typeof opts === 'function') result.push(opts); else if (typeof opts === 'object') result.push(() => opts); return result; }; const chainOrOptions = (keys, opts, defaultOpts) => keys.reduce((acc, item) => ({ ...acc, [item]: opts[item] || defaultOpts[item] }), {}); const chainReservedKeyOptions = (keys, opts, defaultOpts) => keys.reduce((acc, item) => ({ ...acc, [item]: chainOptions(opts[item], defaultOpts[item]), }), {}); const calculateHocsFuncs = (options = {}) => Object.values(options) .filter((item) => typeof item === 'function') .reverse(); /* eslint-disable import/prefer-default-export */ const calculateStyles = (styles) => { if (!styles) return []; return styles.map((item) => item(config.css)); }; /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable no-underscore-dangle */ const cloneAndEnhance = (defaultOpts, opts) => // @ts-ignore rocketComponent({ ...defaultOpts, attrs: chainOptions(opts.attrs, defaultOpts.attrs), filterAttrs: [ ...(defaultOpts.filterAttrs ?? []), ...(opts.filterAttrs ?? []), ], priorityAttrs: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs), statics: { ...defaultOpts.statics, ...opts.statics }, compose: { ...defaultOpts.compose, ...opts.compose }, ...chainOrOptions(CONFIG_KEYS, opts, defaultOpts), ...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts), }); // -------------------------------------------------------- // styleComponent // helper function which allows function chaining // always returns a valid React component with static functions // assigned, so it can be even rendered as a valid component // or styles can be extended via its statics // -------------------------------------------------------- // @ts-ignore const rocketComponent = (options) => { const { component, styles, DEBUG } = options; const { styled } = config; const _calculateStylingAttrs = calculateStylingAttrs({ multiKeys: options.multiKeys, useBooleans: options.useBooleans, }); const componentName = options.name ?? options.component.displayName ?? options.component.name; // create styled component with all options.styles if available const STYLED_COMPONENT = component.IS_ROCKETSTYLE ?? options.styled !== true ? component : styled(component) ` ${calculateStyles(styles)}; `; // -------------------------------------------------------- // COMPONENT - Final component to be rendered // -------------------------------------------------------- const RenderComponent = options.provider ? RocketStyleProviderComponent(STYLED_COMPONENT) : STYLED_COMPONENT; // -------------------------------------------------------- // THEME - Cached & Calculated theme(s) // -------------------------------------------------------- const ThemeManager$1 = new ThemeManager(); // -------------------------------------------------------- // COMPOSE - high-order components // -------------------------------------------------------- const hocsFuncs = [ rocketStyleHOC(options), ...calculateHocsFuncs(options.compose), ]; // -------------------------------------------------------- // ENHANCED COMPONENT (returned component) // -------------------------------------------------------- // .attrs() chaining option is calculated in HOC and passed as props already // @ts-ignore // eslint-disable-next-line react/display-name const EnhancedComponent = forwardRef(({ $rocketstyleRef, // it's forwarded from HOC which is always on top of all hocs ...props }, ref) => { // -------------------------------------------------- // handle refs // (1) one is passed from inner HOC - $rocketstyleRef // (2) second one is used to be used directly (e.g. inside hocs) // -------------------------------------------------- const internalRef = useRocketstyleRef({ $rocketstyleRef, ref }); // -------------------------------------------------- // hover - focus - pressed state passed via context from parent component // -------------------------------------------------- const localCtx = useLocalContext(options.consumer); // -------------------------------------------------- // general theme and theme mode dark / light passed in context // -------------------------------------------------- const { theme, mode } = useThemeAttrs(options); // -------------------------------------------------- // calculate themes for all defined styling dimensions // .theme(...) + defined dimensions like .states(...), .sizes(...), etc. // -------------------------------------------------- // -------------------------------------------------- // BASE / DEFAULT THEME Object // -------------------------------------------------- const baseTheme = useMemo(() => { const helper = ThemeManager$1.baseTheme; if (!helper.has(theme)) { helper.set(theme, getThemeFromChain(options.theme, theme)); } return helper.get(theme); }, // recalculate this only when theme mode changes dark / light [theme]); // -------------------------------------------------- // DIMENSION(S) THEMES Object // -------------------------------------------------- const themes = useMemo(() => { const helper = ThemeManager$1.dimensionsThemes; if (!helper.has(theme)) { helper.set(theme, getDimensionThemes(theme, options)); } return helper.get(theme); }, // recalculate this only when theme object changes [theme]); // -------------------------------------------------- // BASE / DEFAULT MODE THEME Object // -------------------------------------------------- const currentModeBaseTheme = useMemo(() => { const helper = ThemeManager$1.modeBaseTheme[mode]; if (!helper.has(baseTheme)) { helper.set(baseTheme, getThemeByMode(baseTheme, mode)); } return helper.get(baseTheme); }, // recalculate this only when theme mode changes dark / light [mode, baseTheme]); // -------------------------------------------------- // DIMENSION(S) MODE THEMES Object // -------------------------------------------------- const currentModeThemes = useMemo(() => { const helper = ThemeManager$1.modeDimensionTheme[mode]; if (!helper.has(themes)) { helper.set(themes, getThemeByMode(themes, mode)); } return helper.get(themes); }, // recalculate this only when theme mode changes dark / light [mode, themes]); // -------------------------------------------------- // calculate reserved Keys defined in dimensions as styling keys // there is no need to calculate this each time - keys are based on // dimensions definitions // -------------------------------------------------- const { keysMap: dimensions, keywords: reservedPropNames } = useMemo(() => getDimensionsMap({ themes, useBooleans: options.useBooleans, }), [themes]); const RESERVED_STYLING_PROPS_KEYS = useMemo(() => Object.keys(reservedPropNames), [reservedPropNames]); // -------------------------------------------------- // get final props which are (latest has the highest priority): // (1) merged styling from context, // (2) `attrs` chaining method, and from // (3) passing them directly to component // -------------------------------------------------- const { pseudo, ...mergeProps } = { ...localCtx, ...props, }; // -------------------------------------------------- // pseudo rocket state // calculate final component pseudo state including pseudo state // from props and override by pseudo props from context // -------------------------------------------------- const pseudoRocketstate = { ...pseudo, ...pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS]), }; // -------------------------------------------------- // rocketstate // calculate final component state including pseudo state // passed as $rocketstate prop // -------------------------------------------------- const rocketstate = _calculateStylingAttrs({ props: pickStyledAttrs(mergeProps, reservedPropNames), dimensions, }); const finalRocketstate = { ...rocketstate, pseudo: pseudoRocketstate }; // -------------------------------------------------- // rocketstyle // calculated (based on styling props) final theme which will be passed // to our styled component // passed as $rocketstyle prop // -------------------------------------------------- const rocketstyle = getTheme$1({ rocketstate, themes: currentModeThemes, baseTheme: currentModeBaseTheme, }); // -------------------------------------------------- // final props // final props passed to WrappedComponent // excluding: styling props // including: $rocketstyle, $rocketstate // -------------------------------------------------- const finalProps = { // this removes styling state from props and passes its state // under rocketstate key only ...omit(mergeProps, [ ...RESERVED_STYLING_PROPS_KEYS, ...PSEUDO_KEYS, ...options.filterAttrs, ]), // if enforced to pass styling props, we pass them directly ...(options.passProps ? pick(mergeProps, options.passProps) : {}), ref: ref ?? $rocketstyleRef ? internalRef : undefined, // state props passed to styled component only, therefore the `$` symbol $rocketstyle: rocketstyle, $rocketstate: finalRocketstate, }; if (DEBUG && process.env.NODE_ENV !== 'production') { console.log('[Rocketstyle] Debug mode enabled'); console.log(`component ${componentName}`); console.log(finalProps); } // all the development stuff injected if (process.env.NODE_ENV !== 'production') { finalProps['data-rocketstyle'] = componentName; } return React.createElement(RenderComponent, { ...finalProps }); }); // ------------------------------------------------------ // This will hoist and generate dynamically next static methods // for all dimensions available in configuration // ------------------------------------------------------ const RocketComponent = compose(...hocsFuncs)(EnhancedComponent); RocketComponent.IS_ROCKETSTYLE = true; RocketComponent.displayName = componentName; hoistNonReactStatics(RocketComponent, options.component); // ------------------------------------------------------ // enhance for chaining methods // ------------------------------------------------------ createStaticsChainingEnhancers({ context: RocketComponent, dimensionKeys: options.dimensionKeys, func: cloneAndEnhance, options, }); // ------------------------------------------------------ RocketComponent.IS_ROCKETSTYLE = true; RocketComponent.displayName = componentName; RocketComponent.meta = {}; // ------------------------------------------------------ // ------------------------------------------------------ // enhance for statics // ------------------------------------------------------ createStaticsEnhancers({ context: RocketComponent.meta, options: options.statics, }); // @ts-ignore RocketComponent.attrs = (attrs, { priority, filter } = {}) => { const result = {}; if (filter) { result.filterAttrs = filter; } if (priority) { result.priorityAttrs = attrs; return cloneAndEnhance(options, result); } result.attrs = attrs; return cloneAndEnhance(options, result); }; // @ts-ignore RocketComponent.config = (opts = {}) => { const result = pick(opts, CONFIG_KEYS); // @ts-ignore return cloneAndEnhance(options, result); }; RocketComponent.statics = (opts) => // @ts-ignore cloneAndEnhance(options, { statics: opts }); RocketComponent.getStaticDimensions = (theme) => { const themes = getDimensionThemes(theme, options); const { keysMap, keywords } = getDimensionsMap({ themes, useBooleans: options.useBooleans, }); return { dimensions: keysMap, keywords, useBooleans: options.useBooleans, multiKeys: options.multiKeys, }; }; RocketComponent.getDefaultAttrs = (props, theme, mode) => calculateChainOptions(options.attrs)([ props, theme, { render, mode, isDark: mode === 'light', isLight: mode === 'dark', }, ]); return RocketComponent; }; const DEFAULT_DIMENSIONS = { states: 'state', sizes: 'size', variants: 'variant', multiple: { propName: 'multiple', multi: true, }, }; // @ts-nocheck const rocketstyle$1 = ({ dimensions = DEFAULT_DIMENSIONS, useBooleans = true } = {}) => ({ name, component }) => { // -------------------------------------------------------- // handle ERRORS in development mode // -------------------------------------------------------- if (process.env.NODE_ENV !== 'production') { const errors = {}; if (!component) { errors.component = 'Parameter `component` is missing in params!'; } if (!name) { errors.name = 'Parameter `name` is missing in params!'; } if (isEmpty(dimensions)) { errors.dimensions = 'Parameter `dimensions` is missing in params!'; } else { const definedDimensions = getKeys(dimensions); const invalidDimension = ALL_RESERVED_KEYS.some((item) => definedDimensions.includes(item)); if (invalidDimension) { errors.invalidDimensions = `Some of your \`dimensions\` is invalid and uses reserved static keys which are ${DEFAULT_DIMENSIONS.toString()}`; } } if (!isEmpty(errors)) { throw Error(JSON.stringify(errors)); } } return rocketComponent({ name, component, useBooleans, dimensions, dimensionKeys: getKeys(dimensions), dimensionValues: getDimensionsValues(dimensions), multiKeys: getMultipleDimensions(dimensions), styled: true, }); }; const isRocketComponent = (component) => { if (component && typeof component === 'object' && component !== null && Object.prototype.hasOwnProperty.call(component, 'IS_ROCKETSTYLE')) { return true; } return false; }; const Wrapper = config.styled.div ` display: flex; font-size: 32px; `; const component$2 = () => React.createElement(Wrapper, null, "Nothing here"); component$2.displayName = '@vitus-labs/rocketstories/Empty'; var element$1 = rocketstyle$1()({ component: Element, name: 'element', }) .theme({ fontFamily: 'Arial', }) .styles((css) => css ` ${({ $rocketstyle }) => { const baseTheme = styles({ theme: $rocketstyle, css, rootSize: 16 }); return css ` ${baseTheme}; `; }}; `); var Heading = element$1.attrs({ tag: 'h1', block: true }).sizes({ level1: { fontSize: 20, }, level2: { marginTop: 0, fontSize: 16, }, }); /* eslint-disable no-underscore-dangle */ const getTheme = () => window.__VITUS_LABS_STORIES__.decorators.theme; /* eslint-disable no-param-reassign */ const parseProps = (props) => Object.entries(props).reduce((acc, [key, value]) => { if (value === null) return acc; const valueType = typeof value; if (['string', 'number', 'boolean', 'bigint'].includes(valueType)) { return { ...acc, [key]: value }; } if (Array.isArray(value)) { return { ...acc, [key]: value }; } if (valueType === 'object') { const type = get(value, 'type'); const options = get(value, 'options'); const defaultValue = get(value, 'value'); // if has custom knobs configuration if (type && options && defaultValue) { return { ...acc, [key]: defaultValue || options }; } return { ...acc, [key]: value }; } return acc; }, {}); const stringifyArray = (props) => { let result = '['; const arrayLength = props.length; result += props.reduce((acc, value, i) => { if (Array.isArray(value)) { // TODO: parse arrays acc += `${stringifyArray(value)}`; } else if (typeof value === 'object' && value !== null) { acc += `${stringifyObject(value)}`; } else if (['number', 'string'].includes(typeof value)) { acc += `"${value}"`; } else { acc += `${value}`; } // if not last item, add comma and space if (arrayLength !== i + 1) { acc += `, `; } return acc; }, ''); result += ']'; return result; }; const stringifyObject = (props) => { let result = '{ '; const propsArray = Object.entries(props); const arrayLength = propsArray.length; result += propsArray.reduce((acc, [key, value], i) => { if (Array.isArray(value)) { // TODO: parse arrays acc += `${key}: ${value}`; } else if (typeof value === 'object' && value !== null) { acc += `${key}: ${stringifyObject(value)}`; } else if (['string'].includes(typeof value)) { acc += `${key}: "${value}"`; } else { acc += `${key}: ${value}`; } if (arrayLength !== i + 1) { acc += `, `; } return acc; }, ''); result += ' }'; return result; }; const stringifyProps = (props) => { const parsedProps = parseProps(props); const arrayProps = Object.entries(parsedProps); const arrayLength = arrayProps.length; return arrayProps.reduce((acc, [key, value], i) => { if (typeof value === 'boolean') { if (value === true) acc += `${key}`; else acc += `${key}=${value}`; } else if (['string', 'number'].includes(typeof value) || value === null || value === undefined) { acc += `${key}="${value}"`; } else if (Array.isArray(value)) { acc += `${key}={${stringifyArray(value)}}`; } else if (typeof value === 'object' && value !== null) { acc += `${key}={${stringifyObject(value)}}`; } if (arrayLength !== i + 1) { acc += ' '; } return acc; }, ''); }; const parseComponentName = (name) => { const helper = name.split('/'); if (helper.length > 1) { return helper[helper.length - 1]; } return name; }; const createJSXCode = (name, props) => `<${parseComponentName(name)} ${stringifyProps(props)} />`; const createJSXCodeArray = (name, props, dimensionName, dimensions, useBooleans, isMultiKey) => { if (!dimensions) return `// nothing here`; let result = ''; const finalProps = { ...props }; delete finalProps[dimensionName]; result += Object.keys(dimensions).reduce((acc, key) => { acc += createJSXCode(name, { [dimensionName]: isMultiKey ? [key] : key, ...finalProps, }); acc += `\n`; return acc; }, ''); if (useBooleans) { result += `\n\n`; result += `// Or alternatively use boolean ${dimensionName} props (${Object.keys(dimensions).toString()})`; result += `\n`; result += Object.keys(dimensions).reduce((acc, key) => { acc += createJSXCode(name, { [key]: true, ...finalProps }); acc += `\n`; return acc; }, ''); } return result; }; const addBooleanCodeComment = (values) => { let result = `\n\n`; result += `// Or alternatively use boolean props (e.g. ${values})`; result += `\n`; return result; }; const generateMainJSXCode = ({ name, props, dimensions, booleanDimensions, }) => { let result = createJSXCode(name, { ...dimensions, ...props }); if (booleanDimensions) { const keys = Object.keys(booleanDimensions); result += addBooleanCodeComment(keys); result += createJSXCode(name, { ...booleanDimensions, ...props }); } return result; }; const group$4 = 'Element (Vitus-Labs)'; const directionType = 'inline | rows | reverseRows | reverseInline'; const alignXType = 'left | center | right | block | spaceBetween | spaceAround'; const alignYType = 'top | center | block | spaceBetween | spaceAround'; const CssType = 'string | (css) => css`` | css``'; const DIRECTION = { group: group$4, type: 'select', options: ['-----', ...directionType.split(' | ')], value: 'rows', valueType: `${directionType} | Record<string, ${directionType}> | Array<${directionType}`, }; const ALIGN_X = { group: group$4, type: 'select', options: alignXType.split(' | '), value: 'left', valueType: `${alignXType} | Record<string, ${alignXType}> | Array<${alignXType}`, }; const ALIGN_Y = { group: group$4, type: 'select', options: alignYType.split(' | '), value: 'center', valueType: `${alignYType} | Record<string, ${alignYType}> | Array<${alignYType}`, }; const CSS = { group: group$4, type: 'text', valueType: `${CssType} | Record<string,${CssType}> | Array<${CssType}>`, }; var element = { tag: { group: group$4, type: 'select', options: HTML_TAGS, valueType: 'HTMLTag', description: 'A prop which will change **HTML tag** of the element.', }, children: { group: group$4, type: '', valueType: 'ReactNode', description: 'React children. Priorities when rendering are **children** → **content** → **label**, therefore _children_ has the highest priority.', }, content: { group: group$4, type: 'text', valueType: 'ReactNode', description: 'A prop which can be used instead of _children_. Priorities when rendering are **children** → **content** → **label**, therefore _content_ has the middle priority.', }, label: { group: group$4, type: 'text', valueType: 'ReactNode', description: 'A prop which can be used instead of _children_. Priorities when rendering are **children** → **content** → **label**, therefore _label_ has the lowest priority.', }, block: { group: group$4, type: 'boolean', valueType: 'boolean | Record<string, boolean> | Array<boolean>', description: 'Defines whether should behave as **inline** or **block** element.', }, direction: { ...DIRECTION, value: undefined, description: 'Define whether element should render **horizontally** or **vertically**.', }, alignX: { ...ALIGN_X, description: 'Define alignment of **beforeContent**, **content**, and **afterContent** with respect to root element on **axis X**.', }, alignY: { ...ALIGN_Y, description: 'Define alignment of **beforeContent**, **content**, and **afterContent** with respect to the root element on **axis Y**.', }, contentDirection: { ...DIRECTION, description: 'Define whether the children in **content** wrapper should be rendered in _line_ or in _rows_.', }, contentAlignX: { ...ALIGN_X, description: 'Define how the children in **content** wrapper should be aligned on **axis X**.', }, contentAlignY: { ...ALIGN_Y, description: 'Define how the children in **content** wrapper should be aligned on **axis Y**.', }, beforeContentDirection: { ...DIRECTION, description: 'Define whether children in **beforeContent** wrapper should be rendered in _line_ or in _rows_.', }, beforeContentAlignX: { ...ALIGN_X, description: 'Define how children in **beforeContent** wrapper should be aligned on **axis X**.', }, beforeContentAlignY: { ...ALIGN_Y, description: 'Define how children in **beforeContent** wrapper should be aligned on **axis Y**.', }, afterContentDirection: { ...DIRECTION, description: 'Define whether children in **afterContent** wrapper should be rendered in _line_ or in _rows_.', }, afterContentAlignX: { ...ALIGN_X, description: 'Define how children in **afterContent** wrapper should be aligned on **axis X**.', }, afterContentAlignY: { ...ALIGN_Y, description: 'Define how children in **afterContent** wrapper should be aligned on **axis Y**.', }, equalCols: { type: 'boolean', group: group$4, valueType: 'boolean | Record<string,boolean> | Array<boolean>', description: 'Whether should all inner elements have the same `width` / `height`.', }, gap: { type: 'number', group: group$4, valueType: 'number | Record<string,number> | Array<number>', description: 'Defines space gap **between** _beforeContent_, _content_ and _afterContent_ if one of _beforeContent_ or _afterContent_ contain _children_ to be rendered.', }, // vertical: { // type: 'boolean', // group, // valueType: 'boolean | Record<string,boolean> | Array<boolean>', // description: // 'Define whether element should render horizontally or vertically.', // }, beforeContent: { group: group$4, type: '', valueType: 'ReactNode', description: 'A children to be rendered inside `beforeContent` wrapper.', }, afterContent: { group: group$4, type: '', valueType: 'ReactNode', description: 'A children to be rendered inside `afterContent` wrapper.', }, css: { ...CSS, description: 'An additional styling prop to enhance the **root** element CSS styles.', }, contentCss: { ...CSS, description: 'An additional styling prop to enhance the **content** element CSS styles.', }, beforeContentCss: { ...CSS, description: 'An additional styling prop to enhance the **beforeContent** element CSS styles.', }, afterContentCss: { ...CSS, description: 'An additional styling prop to enhance the **afterContent** element CSS styles.', }, ref: { group: group$4, description: 'A React ref', valueType: 'ForwardedRef<any>', }, innerRef: { group: group$4, description: 'A React ref', valueType: 'ForwardedRef<any>', }, dangerouslySetInnerHTML: { group: group$4, type: 'text', disable: true, valueType: 'any', }, }; const group$3 = 'List (@vitus-labs)'; const itemPropsType = `Record<string, any> | (props, meta) => Record<string,any>`; var list = { rootElement: { group: group$3, type: 'boolean', valueType: 'boolean', description: 'Whether a **root** element should be rendered or the output should be just a type of React **Fragment**.', }, data: { group: group$3, type: 'array', valueType: 'string[] | number[] | object[]', description: 'An array of item values to be passed to item component. Data are being passed to _component_ prop element.', }, valueName: { group: group$3, type: 'text', valueType: `string`, description: 'Is required when **data** consists of **strings** or **numbers** to name value being passed as a prop.', }, itemProps: { group: group$3, valueType: itemPropsType, description: 'A customizable hook for dynamically render props for each **item** component.', }, wrapProps: { group: group$3, valueType: itemPropsType, description: 'A customizable hook for dynamically render props for each **wrapComponent** when _wrapComponent_ is passed, otherwise ignored.', }, itemKey: { group: group$3, valueType: 'string | `(item, i) => number | string`', description: "Prop for defining item key in list. **name** / **value** if default behavior doesn't work out.", }, component: { group: group$3, type: 'component', valueType: 'ComponentType', description: 'A component to be rendered within the List per item. Receives props from _data_ array props.', }, wrapComponent: { group: group$3, type: 'component', valueType: `ComponentType`, description: 'A component to be used as a wrapper component for each item component.', }, label: { disable: true, }, content: { disable: true, }, }; const group$2 = 'Overlay (Vitus-Labs)'; var overlay = { refName: { type: 'text', value: 'ref', description: "Overlay component access **ref** to directly mutate styles when calculation position to prevent re-renders. It's being used for both `trigger`, and `children` element at the same time. Your components must accept refs with the same naming.", group: group$2, }, triggerRefName: { type: 'text', description: 'A key name how a **ref** should be passed to trigger component', group: group$2, }, contentRefName: { type: 'text', description: 'A key name how a **ref** should be passed to content component', group: group$2, }, isOpen: { type: 'boolean', value: false, description: '', group: group$2, }, openOn: { type: 'select', options: ['click', 'hover'], value: 'click', description: '', group: group$2, }, closeOn: { type: 'select', options: ['click', 'triggerClick', 'hover', 'manual'], value: 'click', description: '', group: group$2, }, type: { type: 'select', options: ['dropdown', 'tooltip', 'popover', 'modal'], value: 'dropdown', description: '', group: group$2, }, align: { type: 'select', options: ['top', 'left', 'bottom', 'right'], value: 'bottom', description: '', group: group$2, }, alignX: { type: 'select', options: ['left', 'center', 'right'], value: 'left', description: '', group: group$2, }, alignY: { type: 'select', options: ['top', 'center', 'bottom'], value: 'bottom', description: '', group: group$2, }, position: { type: 'select', options: ['fixed', 'absolute', 'relative', 'static'], value: 'fixed', description: '', group: group$2, }, offsetX: { type: 'number', value: 0, description: '', group: group$2, }, offsetY: { type: 'number', value: 0, description: '', group: group$2, }, throttleDelay: { type: 'number', value: 200, description: '', group: group$2, }, children: { description: 'A content to be rendered when Overlay is open', }, }; const group$1 = 'Rocketstyle (Vitus-Labs)'; var rocketstyle = { hover: { group: group$1, type: 'boolean', value: false, description: 'Can be manually triggered **hover** event on the element. Behaves as **:hover** state in _CSS_.', }, active: { group: group$1, type: 'boolean', value: false, description: 'Can be manually triggered **active** event on the element. Can be used to define element as `active`, e.g. _links_.', }, pressed: { group: group$1, type: 'boolean', value: false, description: 'Can be manually triggered **pressed** event on the element. Behaves as `:active` state in _CSS_.', }, focus: { group: group$1, type: 'boolean', value: false, description: 'Can be manually triggered **focus** event on the element. Behaves as `:focus` state in _CSS_.', }, onMouseEnter: { group: group$1, type: 'function', description: 'The _onMouseEnter_ function can take a `SyntheticMouseEvent`.', }, onMouseLeave: { group: group$1, type: 'function', description: 'The _onMouseLeave_ function can take a `SyntheticMouseEvent.`', }, onMouseDown: { group: group$1, type: 'function', description: 'The _onMouseDown_ function can take a `SyntheticMouseEvent`.', }, onMouseUp: { group: group$1, type: 'function', description: 'The _onMouseUp_ function can take a `SyntheticMouseEvent`.', }, onFocus: { group: group$1, type: 'function', description: 'The _onFocus_ function can take a `SyntheticFocusEvent`.', }, onBlur: { group: group$1, type: 'function', description: 'The _onBlur_ function can take a `SyntheticFocusEvent`.', }, }; const group = 'Text (Vitus-Labs)'; var text = { paragraph: { group, type: 'boolean', description: 'Changes a behavior of inline text to become **block** text. Also changes HTML **tag** to `p`', }, tag: { group, type: 'select', options: HTML_TAGS, }, children: { group, type: '', valueType: 'ReactNode', description: 'React children. Priorities when rendering are **children** → **label**, therefore _children_ has the highest priority.', }, label: { group, type: 'text', valueType: 'ReactNode', description: 'A prop which can be used instead of _children_. Priorities when rendering are **children** → **label**, therefore _label_ has lower priority than _children_.', }, extendCss: { group, type: 'text', description: 'An additional styling prop to enhance Text element CSS styles.', }, }; const createControls = (props) => Object.entries(props).reduce((acc, [key, value]) => { if (typeof value === 'string') { return { ...acc, [key]: { type: value, }, }; } if (typeof value === 'object' && value !== null) { return { ...acc, [key]: value }; } return acc; }, {}); const convertDimensionsToControls = ({ dimensions, multiKeys, }) => Object.entries(dimensions).reduce((acc, [key, value]) => { const valueKeys = Object.keys(value); const isMultiKey = !!multiKeys[key]; const control = { type: isMultiKey ? 'multi-select' : 'select', options: valueKeys, group: 'Dimensions [Rocketstyle (Vitus-Labs)]', }; return { ...acc, [key]: control }; }, {}); const getDefaultVitusLabsControls = (component) => { const { IS_ROCKETSTYLE, VITUS_LABS__COMPONENT } = component; cons