Skip to content

Commit

Permalink
feat: #647
Browse files Browse the repository at this point in the history
  • Loading branch information
Mauro Erta committed Apr 13, 2023
1 parent 2d5470e commit d692d13
Show file tree
Hide file tree
Showing 30 changed files with 480 additions and 350 deletions.
11 changes: 11 additions & 0 deletions examples/next/src/components/Button.morfeo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createUseComponent } from '@morfeo/css';

type StyleProps = {
variant: any;
};

export const useButton = createUseComponent({
componentName: 'Button',
py: 'xs',
variant: (props: StyleProps) => props.variant,
});
30 changes: 6 additions & 24 deletions examples/next/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,32 @@
'use client';

import { ButtonHTMLAttributes, DetailedHTMLProps, useState } from 'react';
import { createUseComponent } from '@morfeo/css';
import {
ButtonHTMLAttributes,
CSSProperties,
DetailedHTMLProps,
useState,
} from 'react';
import { Montserrat } from 'next/font/google';
import { lightTheme } from '@morfeo/preset-default';
import { useButton } from './Button.morfeo';

type ButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;

type StyleProps = {
bg: any;
};

const montserrat = Montserrat({
subsets: ['latin'],
});

const useButton = createUseComponent({
componentName: 'Button',
variant: 'primary',
bg: (props: StyleProps) => props.bg,
});

const colors: CSSProperties['color'][] = [
lightTheme.colors.primary,
lightTheme.colors.secondary,
];
const variants = ['primary', 'secondary'] as const;

export const Button: React.FC<ButtonProps> = props => {
const [colorIndex, setColorIndex] = useState<number>(0);
const [variantIndex, setVariantIndex] = useState<number>(0);

const { className, style } = useButton({
bg: colors[colorIndex],
variant: variants[variantIndex],
className: montserrat.className,
style: montserrat.style,
});

function onClick() {
setColorIndex(prev => (prev + 1) % colors.length);
setVariantIndex(prev => (prev + 1) % variants.length);
}

return (
Expand Down
6 changes: 3 additions & 3 deletions examples/next/src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type CardProps = DetailedHTMLProps<
bg?: CSSProperties['color'];
};

const useCard = createUseComponent({
const getCardProps = createUseComponent({
componentName: 'Card',
p: 'm',
transition: 'fast',
Expand All @@ -27,9 +27,9 @@ const useCard = createUseComponent({
},
});

export const Card: React.FC<CardProps> = ({ direction, ...props }) => {
const { className, style } = useCard();
const { className, style } = getCardProps();

export const Card: React.FC<CardProps> = ({ direction, ...props }) => {
return (
<div
{...props}
Expand Down
1 change: 1 addition & 0 deletions packages/babel-plugin/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DYNAMIC_VALUE_TOKEN = 'morfeodynamic';
23 changes: 21 additions & 2 deletions packages/babel-plugin/src/utils/css.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { Style, getStyles } from '@morfeo/web';

function createCSS() {
const cache = new Map<string, string>();
const alreadyInjectedClasses = new Set();

function add(className: string, css: string) {
function updateCache(className: string, css: string) {
if (cache.has(className)) {
return;
}

cache.set(className, css);
}

function add(style: Style) {
const { classes, sheet } = getStyles({
style,
});
const css = sheet.toString();
const className = classes.style;

updateCache(className, css);

return className;
}

function get() {
return Array.from(cache.entries()).reduce((acc, [className, css]) => {
if (alreadyInjectedClasses.has(className)) {
Expand All @@ -22,7 +36,12 @@ function createCSS() {
}, '');
}

return { add, get };
function reset() {
cache.clear();
alreadyInjectedClasses.clear();
}

return { add, get, reset };
}

export const css = createCSS();
108 changes: 108 additions & 0 deletions packages/babel-plugin/src/utils/dynamicClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
theme,
Style,
Property,
ThemeKey,
allPropertiesBySlice,
component,
} from '@morfeo/web';
import { css } from './css';
import { getClassesAndCSS } from './getClassesAndCSS';

function createDynamicClasses() {
const propertyToSliceCache = new Map<Property, ThemeKey>();
const pathToStyleCache = new Map<string, Style>();

function getThemeSliceByProperty(property: Property) {
if (propertyToSliceCache.has(property)) {
return propertyToSliceCache.get(property) as ThemeKey;
}

const sliceName = Object.keys(allPropertiesBySlice).find(sliceName =>
allPropertiesBySlice[sliceName].includes(property),
) as ThemeKey;

propertyToSliceCache.set(property, sliceName);

return sliceName;
}

function createStyleFromPath(path: string, value: any): Style {
const cacheKey = `${path}-${value}`;
if (pathToStyleCache.has(cacheKey)) {
return pathToStyleCache.get(cacheKey) as Style;
}

const [first, ...rest] = path.split('.');

const style = {
[first]:
rest.length > 0 ? createStyleFromPath(rest.join('.'), value) : value,
};

pathToStyleCache.set(cacheKey, style);

return style;
}

function createComponentClasses(
property: 'variant' | 'state',
path: string,
completeStyle: Style,
) {
const componentInfo = component(completeStyle.componentName!);
let componentCss = '';
const variantsOrStates =
componentInfo.get()[property === 'variant' ? 'variants' : 'states'];

const classes = Object.keys(variantsOrStates).reduce(
(acc, variantOrState) => {
const style = createStyleFromPath(path, variantOrState);
const { classes, css } = getClassesAndCSS({
className: {
componentName: completeStyle.componentName,
...(completeStyle.state ? { state: completeStyle.state } : {}),
...(completeStyle.variant
? { variant: completeStyle.variant }
: {}),
...style,
},
});

componentCss += css;

return {
...acc,
[variantOrState]: classes.className,
};
},
{},
);

return { classes, css: componentCss };
}

function create(property: string, path: string, completeStyle: Style) {
if (property === 'variant' || property === 'state') {
return createComponentClasses(property, path, completeStyle);
}

const sliceName = getThemeSliceByProperty(property as Property);
const slice = theme.getSlice(sliceName);
const sliceKeys = Object.keys(slice);
const classes = sliceKeys.reduce((acc, sliceKey) => {
const style = createStyleFromPath(path, sliceKey);

return {
...acc,
[sliceKey]: css.add(style),
};
}, {});

return { classes, css: css.get() };
}

return { create };
}

export const dynamicClasses = createDynamicClasses();
12 changes: 5 additions & 7 deletions packages/babel-plugin/src/utils/getClassesAndCSS.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getStyles, Style } from '@morfeo/web';
import { Style } from '@morfeo/web';
import { css } from './css';
import { splitStyles } from './splitStyles';

Expand All @@ -10,14 +10,12 @@ export function getClassesAndCSS<K extends string>(
const classes = classNames.reduce<Record<K, string>>((acc, className) => {
const splittedStyles = splitStyles(classesStyleObject[className]);

const currentClass = splittedStyles.reduce((acc, style) => {
const { classes, sheet } = getStyles({
[className]: style,
});
const currentClass = splittedStyles.reduce<string>((acc, style) => {
const cssClass = css.add(style);

css.add(classes[className], sheet.toString());
const classes = `${acc} ${cssClass}`.trim();

return `${acc} ${classes[className]}`.trim();
return classes;
}, '');

return { ...acc, [className]: currentClass };
Expand Down
56 changes: 56 additions & 0 deletions packages/babel-plugin/src/utils/getStyleObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ObjectExpression } from '@babel/types';
import generator from '@babel/generator';
import { escapeString } from '@morfeo/utils';
import { isThemeableProperty, toJS } from '../utils';
import { DYNAMIC_VALUE_TOKEN } from '../constants';
import { Property, theme } from '@morfeo/web';

type StyleFunction = {
code: string;
property: string;
variable: string;
};

type ThemeableStyleFunction = {
code: string;
path: string;
property: Property;
};

export function getStyleObject(node: ObjectExpression) {
const styleFunctions: StyleFunction[] = [];
const themeableStyleFunctions: ThemeableStyleFunction[] = [];

const styleObject = toJS(node, {
resolveFunction({ path, property, node }) {
const { code } = generator(node, {
compact: true,
});
const parts = path.split('.');
const variable = `--${escapeString(path)}`;
const isResponsive = theme.isResponsive({ [property]: '' });
// In case the values is responsive, the right property is the parent one.
const propertyToCheck = isResponsive ? parts[parts.length - 2] : property;
const isThemeable = isThemeableProperty(propertyToCheck);

if (isThemeable) {
themeableStyleFunctions.push({
code,
path,
property: propertyToCheck,
});
return DYNAMIC_VALUE_TOKEN;
}

styleFunctions.push({
code,
property,
variable,
});

return `var(${variable})`;
},
});

return { styleObject, styleFunctions, themeableStyleFunctions };
}
3 changes: 3 additions & 0 deletions packages/babel-plugin/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export * from './css';
export * from './toJS';
export * from './splitStyles';
export * from './dynamicClasses';
export * from './getStyleObject';
export * from './getClassesAndCSS';
export * from './isThemeableProperty';
7 changes: 7 additions & 0 deletions packages/babel-plugin/src/utils/isThemeableProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Property, allProperties } from '@morfeo/web';

const componentProperties = ['componentName', 'variant', 'state'];

export function isThemeableProperty(property: string): property is Property {
return !!allProperties[property] || componentProperties.includes(property);
}
Loading

0 comments on commit d692d13

Please sign in to comment.