Skip to content

Commit

Permalink
WIP: UIMix elements as React components (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
seanchas116 committed Apr 30, 2023
1 parent 4753ebb commit ab76991
Show file tree
Hide file tree
Showing 20 changed files with 1,223 additions and 56 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Expand Up @@ -56,6 +56,7 @@
"@types/shelljs": "^0.8.12",
"@types/tmp": "^0.2.3",
"@uimix/adapter-types": "workspace:*",
"@uimix/elements-react": "workspace:*",
"@uimix/foundation": "workspace:*",
"@uimix/model": "workspace:*",
"csstype": "^3.1.2",
Expand Down
15 changes: 4 additions & 11 deletions packages/cli/src/compiler/CSSGenerator.ts
Expand Up @@ -2,13 +2,9 @@ import { kebabCase } from "lodash-es";
import * as CSS from "csstype";
import { Page } from "@uimix/model/src/models/Page";
import * as CodeAsset from "@uimix/adapter-types";
import {
buildNodeCSS,
Selectable,
SelfAndChildrenCSS,
Variant,
} from "@uimix/model/src/models";
import { Selectable, Variant } from "@uimix/model/src/models";
import { ClassNameGenerator } from "./ClassNameGenerator";
import { SelfAndChildrenCSS } from "@uimix/elements-react/src/style";

function isDesignToken(
value: CodeAsset.DesignToken | CodeAsset.DesignTokens
Expand Down Expand Up @@ -70,12 +66,9 @@ export class CSSGenerator {
return css;
}

css = buildNodeCSS(
selectable.node.type,
selectable.style,
css = selectable.buildCSS(
(tokenID) =>
getColorToken(this.designTokens, tokenID.split("/"))?.$value ??
selectable.project.colorTokens.resolve(tokenID)
getColorToken(this.designTokens, tokenID.split("/"))?.$value
);
const isTopLevel =
selectable.idPath.length === 1 &&
Expand Down
6 changes: 2 additions & 4 deletions packages/editor/src/views/viewport/TextEditor.tsx
@@ -1,7 +1,7 @@
import { action, reaction } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useRef } from "react";
import { Selectable, buildNodeCSS } from "@uimix/model/src/models";
import { Selectable } from "@uimix/model/src/models";
import { viewportState } from "../../state/ViewportState";
import { projectState } from "../../state/ProjectState";

Expand All @@ -10,9 +10,7 @@ export const TextEditorBody: React.FC<{
}> = observer(({ selectable }) => {
const style = selectable.style;

const cssStyle = buildNodeCSS("text", style, (tokenID) =>
selectable.project.colorTokens.resolve(tokenID)
).self;
const cssStyle = selectable.buildCSS().self;
delete cssStyle.marginTop;
delete cssStyle.marginRight;
delete cssStyle.marginBottom;
Expand Down
6 changes: 2 additions & 4 deletions packages/editor/src/views/viewport/renderer/NodeRenderer.tsx
@@ -1,6 +1,6 @@
import React, { createRef, useEffect, useRef } from "react";
import { observer } from "mobx-react-lite";
import { Selectable, buildNodeCSS } from "@uimix/model/src/models";
import { Selectable } from "@uimix/model/src/models";
import { viewportState } from "../../../state/ViewportState";
import { ComputedRectProvider } from "./ComputedRectProvider";
import { projectState } from "../../../state/ProjectState";
Expand Down Expand Up @@ -59,9 +59,7 @@ export const NodeRenderer: React.FC<{
const style = selectable.style;
const type = selectable.node.type;

const builtStyle = buildNodeCSS(type, style, (tokenID) =>
selectable.project.colorTokens.resolve(tokenID)
);
const builtStyle = selectable.buildCSS();

const cssStyle: React.CSSProperties = {
all: "revert",
Expand Down
16 changes: 16 additions & 0 deletions packages/elements-react/.babelrc.json
@@ -0,0 +1,16 @@
{
"sourceType": "unambiguous",
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": 100
}
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": []
}
17 changes: 17 additions & 0 deletions packages/elements-react/.storybook/main.ts
@@ -0,0 +1,17 @@
import type { StorybookConfig } from "@storybook/react-webpack5";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-webpack5",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;
15 changes: 15 additions & 0 deletions packages/elements-react/.storybook/preview.ts
@@ -0,0 +1,15 @@
import type { Preview } from "@storybook/react";

const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};

export default preview;
33 changes: 33 additions & 0 deletions packages/elements-react/package.json
@@ -0,0 +1,33 @@
{
"name": "@uimix/elements-react",
"license": "MIT",
"type": "module",
"main": "src/index.ts",
"scripts": {
"lint": "eslint src",
"storybook": "storybook dev -p 8008",
"build-storybook": "storybook build"
},
"dependencies": {
"csstype": "^3.1.2"
},
"devDependencies": {
"@babel/preset-env": "^7.21.4",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.4",
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-interactions": "^7.0.7",
"@storybook/addon-links": "^7.0.7",
"@storybook/blocks": "^7.0.7",
"@storybook/react": "^7.0.7",
"@storybook/react-webpack5": "^7.0.7",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/react": "^18.0.38",
"@types/react-dom": "^18.0.11",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^7.0.7",
"typescript": "^5.0.4"
}
}
29 changes: 29 additions & 0 deletions packages/elements-react/src/components/Box.stories.tsx
@@ -0,0 +1,29 @@
import { Box, Text } from "./Box";

export default {
title: "Box",
component: Box,
};

export const Basic = () => {
return <Box width={100} height={200} fills={[{ solid: "#c0ffee" }]} />;
};

export const WithText = () => {
return (
<Box
width={100}
height={200}
fills={[{ solid: "#c0ffee" }]}
layout="flex"
flexDirection="y"
rowGap={20}
paddingLeft={10}
paddingRight={10}
>
<Box width={100} height={100} fills={[{ solid: "#f0f0f0" }]} />
<Text fills={[{ solid: "#000" }]} textContent="Hello, world!" />
<Text fills={[{ solid: "#666" }]} textContent="This is a text" />
</Box>
);
};
73 changes: 73 additions & 0 deletions packages/elements-react/src/components/Box.tsx
@@ -0,0 +1,73 @@
import { ReactNode, useId } from "react";
import {
StyleProps,
defaultStyle,
SelfAndChildrenCSS,
buildNodeCSS,
} from "../style";

function kebabCase(str: string): string {
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
}

export const Box: React.FC<
Partial<StyleProps> & {
children?: ReactNode;
}
> = (props) => {
const styles = buildNodeCSS("frame", {
...defaultStyle,
...props,
});

const id = useId().replaceAll(":", "_");
const className = `box-${id}`;

return (
<div className={className}>
<Style targetClassName={className} styles={styles} />
{props.children}
</div>
);
};

export const Text: React.FC<
Partial<StyleProps> & {
children?: ReactNode;
}
> = (props) => {
const styles = buildNodeCSS("text", {
...defaultStyle,
...props,
});

const id = useId().replaceAll(":", "_");
const className = `box-${id}`;

return (
<div className={className}>
<Style targetClassName={className} styles={styles} />
{props.textContent}
</div>
);
};

const Style: React.FC<{
targetClassName: string;
styles: SelfAndChildrenCSS;
}> = ({ styles, targetClassName }) => {
const cssBody = Object.entries(styles.self)
.map(([key, value]) => {
return `${kebabCase(key)}: ${String(value)};`;
})
.join(";");
const childrenCSSBody = Object.entries(styles.children)
.map(([key, value]) => {
return `${kebabCase(key)}: ${String(value)};`;
})
.join(";");

const styleText = `.${targetClassName}{${cssBody}} .${targetClassName}>*{${childrenCSSBody}}`;

return <style>{styleText}</style>;
};
1 change: 1 addition & 0 deletions packages/elements-react/src/components/index.ts
@@ -0,0 +1 @@
export * from "./Box";
2 changes: 2 additions & 0 deletions packages/elements-react/src/index.ts
@@ -0,0 +1,2 @@
export * from "./components";
export * from "./style";

2 comments on commit ab76991

@vercel
Copy link

@vercel vercel bot commented on ab76991 Apr 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on ab76991 Apr 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.