diff --git a/examples/responsive/.babelrc b/examples/responsive/.babelrc new file mode 100644 index 000000000..a6f4434e2 --- /dev/null +++ b/examples/responsive/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/examples/responsive/.gitignore b/examples/responsive/.gitignore new file mode 100644 index 000000000..922d92a57 --- /dev/null +++ b/examples/responsive/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/responsive/README.md b/examples/responsive/README.md new file mode 100644 index 000000000..248491ee8 --- /dev/null +++ b/examples/responsive/README.md @@ -0,0 +1,5 @@ +# Responsive Design Example + +This is the source code for the responsive guide seen [here](https://craft.js.org/) + +> The code is admittedly a bit messy and is scheduled for a clean up. In the mean time, feel free to submit an issue if you encounter any confusing/weird/wtf bits ... or even better, submit a pull request! :clap: diff --git a/examples/responsive/components/selectors/Button/index.tsx b/examples/responsive/components/selectors/Button/index.tsx new file mode 100644 index 000000000..44e595810 --- /dev/null +++ b/examples/responsive/components/selectors/Button/index.tsx @@ -0,0 +1,68 @@ +import { UserComponent, useNode } from '@craftjs/core'; +import cx from 'classnames'; +import React from 'react'; +import styled from 'styled-components'; + +import { Text } from '../Text'; + +type ButtonProps = { + background?: Record<'r' | 'g' | 'b' | 'a', number>; + color?: Record<'r' | 'g' | 'b' | 'a', number>; + buttonStyle?: string; + margin?: any[]; + text?: string; + textComponent?: any; +}; + +const StyledButton = styled.button` + background: ${(props) => + props.buttonStyle === 'full' + ? `rgba(${Object.values(props.background)})` + : 'transparent'}; + border: 2px solid transparent; + border-color: ${(props) => + props.buttonStyle === 'outline' + ? `rgba(${Object.values(props.background)})` + : 'transparent'}; + margin: ${({ margin }) => + `${margin[0]}px ${margin[1]}px ${margin[2]}px ${margin[3]}px`}; +`; + +export const Button: UserComponent = (props: any) => { + const { + connectors: { connect }, + } = useNode((node) => ({ + selected: node.events.selected, + })); + + const { text, textComponent, color, ...otherProps } = props; + return ( + + + + ); +}; + +Button.craft = { + displayName: 'Button', + props: { + background: { r: 255, g: 255, b: 255, a: 0.5 }, + color: { r: 92, g: 90, b: 90, a: 1 }, + buttonStyle: 'full', + text: 'Button', + margin: ['5', '0', '5', '0'], + textComponent: { + ...Text.craft.props, + textAlign: 'center', + }, + }, +}; diff --git a/examples/responsive/components/selectors/Container/index.tsx b/examples/responsive/components/selectors/Container/index.tsx new file mode 100644 index 000000000..41e5849a6 --- /dev/null +++ b/examples/responsive/components/selectors/Container/index.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +export type ContainerProps = { + background: Record<'r' | 'g' | 'b' | 'a', number>; + color: Record<'r' | 'g' | 'b' | 'a', number>; + flexDirection: string; + alignItems: string; + justifyContent: string; + fillSpace: string; + width: string; + height: string; + padding: string[]; + margin: string[]; + marginTop: number; + marginLeft: number; + marginBottom: number; + marginRight: number; + shadow: number; + children: React.ReactNode; + radius: number; +}; + +const defaultProps = { + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'flex-start', + fillSpace: 'no', + padding: ['0', '0', '0', '0'], + margin: ['0', '0', '0', '0'], + background: { r: 255, g: 255, b: 255, a: 1 }, + color: { r: 0, g: 0, b: 0, a: 1 }, + shadow: 0, + radius: 0, + width: '100%', + height: 'auto', +}; + +export const Container = (props: Partial) => { + props = { + ...defaultProps, + ...props, + }; + const { + // flexDirection, + // alignItems, + // justifyContent, + // fillSpace, + background, + // color, + // padding, + // margin, + shadow, + // radius, + children, + } = props; + return ( +
+ {children} +
+ ); +}; + +Container.craft = { + displayName: 'Container', + props: defaultProps, + rules: { + canDrag: () => true, + }, +}; diff --git a/examples/responsive/components/selectors/Text/index.tsx b/examples/responsive/components/selectors/Text/index.tsx new file mode 100644 index 000000000..97105f5e6 --- /dev/null +++ b/examples/responsive/components/selectors/Text/index.tsx @@ -0,0 +1,64 @@ +import { useNode, useEditor } from '@craftjs/core'; +import React from 'react'; +import ContentEditable from 'react-contenteditable'; + +export type TextProps = { + fontSize: string; + textAlign: string; + fontWeight: string; + color: Record<'r' | 'g' | 'b' | 'a', string>; + shadow: number; + text: string; + margin: [string, string, string, string]; +}; + +export const Text = ({ + fontSize, + textAlign, + fontWeight, + color, + shadow, + text, + margin, +}: Partial) => { + const { + connectors: { connect }, + setProp, + } = useNode(); + const { enabled } = useEditor((state) => ({ + enabled: state.options.enabled, + })); + return ( + { + setProp((prop) => (prop.text = e.target.value), 500); + }} // use true to disable editing + tagName="h2" // Use a custom HTML tag (uses a div by default) + style={{ + width: '100%', + margin: `${margin[0]}px ${margin[1]}px ${margin[2]}px ${margin[3]}px`, + color: `rgba(${Object.values(color)})`, + fontSize: `${fontSize}px`, + textShadow: `0px 0px 2px rgba(0,0,0,${(shadow || 0) / 100})`, + fontWeight, + textAlign, + }} + /> + ); +}; + +Text.craft = { + displayName: 'Text', + props: { + fontSize: '15', + textAlign: 'left', + fontWeight: '500', + color: { r: 92, g: 90, b: 90, a: 1 }, + margin: [0, 0, 0, 0], + shadow: 0, + text: 'Text', + }, +}; diff --git a/examples/responsive/components/selectors/index.ts b/examples/responsive/components/selectors/index.ts new file mode 100644 index 000000000..8dd533d20 --- /dev/null +++ b/examples/responsive/components/selectors/index.ts @@ -0,0 +1,2 @@ +export * from './Container'; +export * from './Text'; diff --git a/examples/responsive/next-env.d.ts b/examples/responsive/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/examples/responsive/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/responsive/next.config.js b/examples/responsive/next.config.js new file mode 100644 index 000000000..ea48d4656 --- /dev/null +++ b/examples/responsive/next.config.js @@ -0,0 +1,4 @@ +module.exports = { + assetPrefix: + process.env.NODE_ENV === 'production' ? '/examples/landing' : '/', +}; diff --git a/examples/responsive/package.json b/examples/responsive/package.json new file mode 100644 index 000000000..a6da0d08d --- /dev/null +++ b/examples/responsive/package.json @@ -0,0 +1,50 @@ +{ + "name": "example-responsive", + "version": "0.2.0", + "private": true, + "scripts": { + "start": "next dev -p 3002", + "build": "next build", + "export": "next export", + "clean": "rimraf lib .next out dist" + }, + "dependencies": { + "@craftjs/core": "workspace:*", + "@craftjs/layers": "workspace:*", + "@material-ui/core": "4.5.2", + "@material-ui/icons": "4.5.1", + "autoprefixer": "latest", + "classnames": "2.2.6", + "cssnano": "4.1.10", + "debounce": "1.2.0", + "lzutf8": "0.5.5", + "next": "13.1.6", + "next-seo": "4.24.0", + "postcss": "latest", + "re-resizable": "6.1.0", + "react": "18.2.0", + "react-color": "2.17.3", + "react-contenteditable": "3.3.2", + "react-dom": "18.2.0", + "react-frame-component": "^5.2.6", + "react-loading": "2.0.3", + "react-rnd": "10.1.1", + "react-youtube": "7.9.0", + "styled-components": "4.4.1" + }, + "devDependencies": { + "@babel/core": "7.7.5", + "@fullhuman/postcss-purgecss": "1.3.0", + "@types/classnames": "2.2.9", + "@types/node": "12.12.5", + "@types/react": "18.0.27", + "@types/react-color": "3.0.1", + "@types/styled-components": "4.4.1", + "babel-plugin-inline-react-svg": "2.0.1", + "cross-env": "6.0.3", + "postcss-import": "12.0.1", + "postcss-preset-env": "6.7.0", + "tailwindcss": "latest", + "typescript": "4.9.5" + } +} diff --git a/examples/responsive/pages/_app.tsx b/examples/responsive/pages/_app.tsx new file mode 100644 index 000000000..97a6c4d58 --- /dev/null +++ b/examples/responsive/pages/_app.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import '../styles/app.css'; + +function MyApp({ Component, pageProps }) { + return ; +} + +// Only uncomment this method if you have blocking data requirements for +// every single page in your application. This disables the ability to +// perform automatic static optimization, causing every page in your app to +// be server-side rendered. +// +// MyApp.getInitialProps = async (appContext) => { +// // calls page's `getInitialProps` and fills `appProps.pageProps` +// const appProps = await App.getInitialProps(appContext); +// +// return { ...appProps } +// } + +export default MyApp; diff --git a/examples/responsive/pages/_document.tsx b/examples/responsive/pages/_document.tsx new file mode 100644 index 000000000..0da52da1b --- /dev/null +++ b/examples/responsive/pages/_document.tsx @@ -0,0 +1,32 @@ +import Document, { DocumentContext, DocumentInitialProps } from 'next/document'; +import { ServerStyleSheet } from 'styled-components'; + +export default class MyDocument extends Document { + static async getInitialProps( + ctx: DocumentContext + ): Promise { + const sheet = new ServerStyleSheet(); + const originalRenderPage = ctx.renderPage; + + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App) => (props) => + sheet.collectStyles(), + }); + + const initialProps = await Document.getInitialProps(ctx); + return { + ...initialProps, + styles: ( + <> + {initialProps.styles} + {sheet.getStyleElement()} + + ), + }; + } finally { + sheet.seal(); + } + } +} diff --git a/examples/responsive/pages/index.tsx b/examples/responsive/pages/index.tsx new file mode 100644 index 000000000..3bcb3123a --- /dev/null +++ b/examples/responsive/pages/index.tsx @@ -0,0 +1,100 @@ +import { Editor, Frame, Element } from '@craftjs/core'; +import { Button, ButtonGroup, createMuiTheme } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/core/styles'; +import dynamic from 'next/dynamic'; +import type { ReactNode } from 'react'; +import React, { useState } from 'react'; +import { useLayoutEffect } from 'react'; +import IFrame from 'react-frame-component'; +import { useFrame } from 'react-frame-component'; + +import { Container, Text } from '../components/selectors'; + +const theme = createMuiTheme({ + typography: { + fontFamily: [ + 'acumin-pro', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + }, +}); + +function App() { + const breakpoints = { + sm: '640px', // small devices i.e phones + md: '1280px', // medium devices i.e tablets + lg: '1536px', // large devices i.e computers,laptops... + }; + + const [breakpoint, setBreakpoint] = useState('lg'); + + return ( + +
+ +
+ + + + + +
+ +
+ +
+
+
+
+ ); +} +function AddStyles({ children }: { children: ReactNode }) { + const { document: doc } = useFrame(); // + +``` + +> **NOTE: if using SSR framework like next** +> You must ensure that this component runs only on the client + +If you did this correctly you should be able you should see the text on the screen and be able to resize the iframe correctly. **But, and this is important** the text will still not change color. So, we are back where we started, see? progress. This is because of the [Iframe CSS Problem](#the-iframe-css-problem) + +### The iframe CSS Problem +An **iframe is a website inside your website, therefore it does not have access to your website's style sheets.** + +**This is a very complex issue**, fixing it will largely depend on what styling solution you have chosen & how you implement styling in your editor. The general ideal is that you need to add a stylesheet with the styling you need for your editor to function to the iframe. + +**Here are some solutions based on common patterns** +1. **CSS-in-JS Solution**: if using something like `Styled Components` where all `User Components` are styled with `styled-components`, in which case - the stylesheet for those components are generated on runtime and is injected into the parent document rather than inside inside the iframe. The easiest solution is to clone the stylesheet into the iframe. +2. **Using tailwind or other CSS library**: You can clone the stylesheet into the iframe. Or you can add the tailwind/bootstrap/... cdn stylesheet to the iframe + +Either way you will probably have to look around, decide on a solution and implement it. + +**To clone a stylesheet from your document into the iframe** you can just get a reference to the HTML element & append it to the head of the iframe (react-frame-component provides a nice hook that gives you a reference the the iframe document). + + +### Solution +it will copy all the stylesheets from your nextjs app into the iframe and now, **finally, it works!!!! But don't go yet, we still have problem #2 to tackle, [how to let the user specify responsive styles](#the-problem-of-styles).** + + + +Here is the code up to this point + +```tsx +import { Editor, Frame, Element } from '@craftjs/core'; +import { Button, ButtonGroup, createMuiTheme } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/core/styles'; +import React, { useState } from 'react'; + +import { Container, Text } from '../components/selectors'; +import IFrame from 'react-frame-component'; + +const theme = createMuiTheme({ + typography: { + fontFamily: [ + 'acumin-pro', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + }, +}); + +function App() { + const breakpoints = { + sm: '640px', // small devices i.e phones + md: '1280px', // medium devices i.e tablets + lg: '1536px', // large devices i.e computers,laptops... + }; + + const [breakpoint, setBreakpoint] = useState('lg'); + + return ( + +
+ +
+ + + + + +
+ +
+ +
+
+
+
+ ); +} + +import type { ReactNode } from 'react'; +import { useFrame } from 'react-frame-component'; +import { useLayoutEffect } from 'react'; +function AddStyles({ children }: { children: ReactNode }) { + const { document: doc } = useFrame(); //