├── .prettierrc ├── playground ├── types │ ├── babel-standalone.d.ts │ ├── babel-plugin-unpkg.d.ts │ ├── react-inspector.d.ts │ ├── styled-components.d.ts │ └── iframe.d.ts ├── README.md ├── .babelrc ├── src │ ├── types.ts │ ├── Result │ │ ├── ErrorDisplay.tsx │ │ ├── Console.tsx │ │ ├── Frame.tsx │ │ └── index.tsx │ ├── utils │ │ ├── media.ts │ │ ├── theme.ts │ │ └── constructSnippet.ts │ ├── __tests__ │ │ ├── Editor.test.tsx │ │ └── Frame.test.tsx │ ├── Draggable │ │ ├── index.tsx │ │ └── useDrag.ts │ ├── TabStyles.tsx │ ├── Editor │ │ ├── index.tsx │ │ └── EditorSetup.tsx │ └── Playground.tsx ├── tsconfig.json └── package.json ├── .vscode └── settings.json ├── assets └── icon.png ├── .github └── workflows │ └── nodejs.yml ├── example ├── package.json └── src │ ├── index.html │ └── index.js ├── package.json ├── LICENSE ├── .gitignore └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | -------------------------------------------------------------------------------- /playground/types/babel-standalone.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@babel/standalone"; 2 | -------------------------------------------------------------------------------- /playground/types/babel-plugin-unpkg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "babel-plugin-unpkg"; 2 | -------------------------------------------------------------------------------- /playground/types/react-inspector.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@agney/react-inspector"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/playground/HEAD/assets/icon.png -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | > Read complete documentation on [Github](https://github.com/agneym/playground). 2 | -------------------------------------------------------------------------------- /playground/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /playground/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | 3 | import { theme } from "../src/utils/theme"; 4 | 5 | declare module "styled-components" { 6 | type Theme = typeof theme; 7 | export interface DefaultTheme extends Theme {} 8 | } 9 | -------------------------------------------------------------------------------- /playground/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ISnippet { 2 | markup: string; 3 | css: string; 4 | javascript: string; 5 | } 6 | 7 | export type IEditorTabs = "markup" | "css" | "javascript"; 8 | export type IResultTabs = "result" | "console"; 9 | 10 | export interface ITabConfig { 11 | name: string; 12 | value: T; 13 | } 14 | -------------------------------------------------------------------------------- /playground/types/iframe.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface IFrameLazy 3 | extends React.DetailedHTMLProps< 4 | React.IframeHTMLAttributes, 5 | HTMLIFrameElement 6 | > { 7 | loading: "eager" | "lazy" | "auto"; 8 | } 9 | interface IntrinsicElements { 10 | iframe: IFrameLazy; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "lib": ["es2017", "dom"], 7 | "noEmitOnError": true, 8 | "strict": false, 9 | "target": "es5", 10 | "typeRoots": ["./node_modules/@types", "../node_modules/@types", "./types"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install, build, and test 21 | run: | 22 | yarn 23 | yarn run build --if-present 24 | yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /playground/src/Result/ErrorDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | background-color: ${props => props.theme.error.background}; 6 | color: ${props => props.theme.error.color}; 7 | padding: 0.2em 0.5em; 8 | position: absolute; 9 | width: 100%; 10 | bottom: 0; 11 | box-sizing: border-box; 12 | `; 13 | 14 | interface IProps { 15 | error: string; 16 | } 17 | 18 | const ErrorDisplay: FC = ({ error }) => { 19 | return ( 20 | 21 |

{error}

22 |
23 | ); 24 | }; 25 | 26 | export default ErrorDisplay; 27 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "parcel src/index.html", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "Agney Menon (@agneymenon)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@agney/playground": "*", 16 | "babel-polyfill": "^6.26.0", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1" 19 | }, 20 | "devDependencies": { 21 | "parcel-bundler": "^1.12.4" 22 | }, 23 | "browserslist": [ 24 | "last 3 versions", 25 | "> 3%" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /playground/src/Result/Console.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "styled-components"; 3 | import Inspector from "@agney/react-inspector"; 4 | 5 | const Container = styled.div` 6 | background-color: ${props => props.theme.console.background}; 7 | height: 100%; 8 | 9 | li { 10 | font-size: 16px !important; 11 | } 12 | `; 13 | 14 | interface IProps { 15 | logs: unknown[]; 16 | } 17 | 18 | const Console: FC = ({ logs }) => { 19 | return ( 20 | 21 | {logs.map((log: unknown, index: number) => ( 22 | 23 | ))} 24 | 25 | ); 26 | }; 27 | 28 | export default Console; 29 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Playground example 8 | 14 | 15 | 16 | 17 |
18 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /playground/src/utils/media.ts: -------------------------------------------------------------------------------- 1 | import { ThemedCssFunction, css, DefaultTheme } from "styled-components"; 2 | 3 | interface ISize { 4 | [K: string]: number; 5 | } 6 | 7 | const sizes: Readonly = { 8 | desktop: 992, 9 | tablet: 768, 10 | phone: 576, 11 | }; 12 | 13 | // Iterate through the sizes and create a media template 14 | const media = (Object.keys(sizes) as (keyof typeof sizes)[]).reduce( 15 | (acc, label) => { 16 | acc[label] = (first: any, ...interpolations: any[]) => css` 17 | @media (max-width: ${sizes[label]}px) { 18 | ${css(first, ...interpolations)} 19 | } 20 | `; 21 | 22 | return acc; 23 | }, 24 | {} as { [key in keyof typeof sizes]: ThemedCssFunction } 25 | ); 26 | 27 | export default media; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-workspace", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "yarn workspace @agney/playground start", 7 | "start-example": "yarn workspace example start", 8 | "build": "yarn workspace @agney/playground build", 9 | "test": "yarn workspace @agney/playground test" 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "pretty-quick --staged" 14 | } 15 | }, 16 | "workspaces": [ 17 | "playground", 18 | "example" 19 | ], 20 | "devDependencies": { 21 | "husky": "^4.2.5", 22 | "prettier": "^2.0.5", 23 | "pretty-quick": "^2.0.1", 24 | "typescript": "^3.9.5" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:agneym/playground.git" 29 | }, 30 | "dependencies": {} 31 | } 32 | -------------------------------------------------------------------------------- /playground/src/__tests__/Editor.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import Editor from "../Editor"; 4 | import { ThemeProvider } from "styled-components"; 5 | import getTheme from "../utils/theme"; 6 | 7 | const initialSnippet = { 8 | markup: ``, 9 | css: ``, 10 | javascript: ``, 11 | }; 12 | 13 | describe("Editor", () => { 14 | it("should render the default tab as per prop", () => { 15 | const defaultTab = "css"; 16 | const { getByText } = render( 17 | 18 | {}} 23 | /> 24 | 25 | ); 26 | const button = getByText("CSS"); 27 | expect(button.getAttribute("data-selected")).toBe(""); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { render } from "react-dom"; 3 | import "babel-polyfill"; 4 | import Playground from "@agney/playground"; 5 | 6 | const App = () => { 7 | const snippet = { 8 | markup: `
`, 9 | css: ``, 10 | javascript: `import { h, Component, render } from 'preact'; 11 | import htm from 'htm'; 12 | 13 | const html = htm.bind(h); 14 | 15 | const app = html\`
Hello World from Playground!
\` 16 | 17 | render(app, document.getElementById('app')); 18 | `, 19 | }; 20 | return ( 21 |
22 | 29 |
30 | ); 31 | }; 32 | 33 | const rootEl = document.getElementById("root"); 34 | render(, rootEl); 35 | -------------------------------------------------------------------------------- /playground/src/__tests__/Frame.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { ThemeProvider } from "styled-components"; 4 | import getTheme from "../utils/theme"; 5 | 6 | import Frame from "../Result/Frame"; 7 | 8 | jest.mock("../utils/constructSnippet", () => { 9 | return jest.fn().mockImplementation(() => { 10 | throw new Error("error"); 11 | }); 12 | }); 13 | 14 | const initialSnippet = { 15 | markup: ``, 16 | css: ``, 17 | javascript: ``, 18 | }; 19 | 20 | describe("Frame", () => { 21 | it("should render error", () => { 22 | const { getByText } = render( 23 | 24 | 30 | 31 | ); 32 | expect(getByText("error")).toBeDefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Agney Menon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /playground/src/Draggable/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode, useRef, useContext } from "react"; 2 | import styled, { ThemeContext } from "styled-components"; 3 | 4 | import useDrag from "./useDrag"; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | align-items: stretch; 9 | `; 10 | 11 | const Divider = styled.div` 12 | width: ${props => props.theme.divider.width}px; 13 | cursor: col-resize; 14 | background-color: ${props => props.theme.divider.background}; 15 | `; 16 | 17 | interface IProps { 18 | className?: string; 19 | leftChild: (width: number) => ReactNode; 20 | rightChild: (width: number) => ReactNode; 21 | } 22 | 23 | const Draggable: FC = ({ className = "", leftChild, rightChild }) => { 24 | const containerRef = useRef(null); 25 | const dividerRef = useRef(null); 26 | const themeContext = useContext(ThemeContext); 27 | 28 | const { leftWidth, rightWidth } = useDrag({ 29 | containerRef, 30 | dividerRef, 31 | dividerWidth: themeContext.divider.width, 32 | }); 33 | 34 | return ( 35 | 36 | {leftChild(leftWidth)} 37 | 38 | {rightChild(rightWidth)} 39 | 40 | ); 41 | }; 42 | 43 | export default Draggable; 44 | -------------------------------------------------------------------------------- /playground/src/TabStyles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs"; 3 | 4 | import media from "./utils/media"; 5 | 6 | export const StyledTabs = styled(Tabs)` 7 | display: flex; 8 | flex-direction: column; 9 | width: 50%; 10 | min-width: ${props => props.theme.container.minWidth}; 11 | 12 | ${media.phone` 13 | width: 100%; 14 | `} 15 | `; 16 | 17 | export const StyledTabList = styled(TabList)` 18 | border-bottom: ${props => props.theme.tabs.tabHeader.borderBottom}; 19 | padding: 0 0.8em; 20 | background-color: ${props => props.theme.tabs.tabHeader.panelBackground || 'transparent'}; 21 | `; 22 | 23 | export const StyledTab = styled(Tab)` 24 | background-color: ${props => props.theme.tabs.tabHeader.background}; 25 | border: none; 26 | padding: 0.8em 0.5em; 27 | margin: 0 0.2em; 28 | cursor: pointer; 29 | color: ${props => props.theme.tabs.tabHeader.color}; 30 | 31 | &[data-selected] { 32 | border-bottom: ${props => props.theme.tabs.selectedTab.borderBottom}; 33 | } 34 | `; 35 | 36 | export const StyledTabPanels = styled(TabPanels)` 37 | flex: 1; 38 | 39 | ${media.phone` 40 | height: ${props => props.theme.tabs.tabPanel.phoneHeight}; 41 | `} 42 | `; 43 | 44 | export const StyledTabPanel = styled(TabPanel)` 45 | height: 100%; 46 | `; 47 | -------------------------------------------------------------------------------- /playground/src/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from "react"; 2 | import styled from "styled-components"; 3 | import { IEditorTabs, ISnippet } from "../types"; 4 | import EditorSetup from "./EditorSetup"; 5 | import { ITabConfig } from "../types"; 6 | import { 7 | StyledTabs, 8 | StyledTabList, 9 | StyledTab, 10 | StyledTabPanels, 11 | StyledTabPanel, 12 | } from "../TabStyles"; 13 | 14 | const TabContainer = styled(StyledTabs)` 15 | min-width: ${props => props.theme.container.minWidth}; 16 | `; 17 | 18 | interface IProps { 19 | width: number; 20 | code: ISnippet; 21 | defaultTab: IEditorTabs; 22 | onChange: (changed: string, type: IEditorTabs) => void; 23 | } 24 | 25 | const Editor: FC = ({ code, defaultTab, onChange, width }) => { 26 | const tabs: Readonly[]> = useMemo( 27 | () => [ 28 | { name: "HTML", value: "markup" }, 29 | { name: "CSS", value: "css" }, 30 | { name: "JS", value: "javascript" }, 31 | ], 32 | [] 33 | ); 34 | return ( 35 | tab.value === defaultTab)} 37 | style={{ width: width }} 38 | > 39 | 40 | {tabs.map(tab => ( 41 | {tab.name} 42 | ))} 43 | 44 | 45 | {tabs.map(tab => ( 46 | 47 | 52 | 53 | ))} 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Editor; 60 | -------------------------------------------------------------------------------- /playground/src/Editor/EditorSetup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, Fragment } from "react"; 2 | import SimpleEditor from "react-simple-code-editor"; 3 | import Highlight, { defaultProps } from "prism-react-renderer"; 4 | import theme from "prism-react-renderer/themes/nightOwl"; 5 | import styled from "styled-components"; 6 | 7 | import { IEditorTabs } from "../types"; 8 | 9 | const StyledSimpleEditor = styled(SimpleEditor)` 10 | background-color: ${props => props.theme.editor.backgroundColor}; 11 | color: ${props => props.theme.editor.color}; 12 | overflow-y: auto !important; 13 | font-family: ${props => props.theme.editor.fontFamily}; 14 | font-feature-settings: normal; 15 | `; 16 | 17 | interface IProps { 18 | code: string; 19 | language: IEditorTabs; 20 | onChange: (value: string, language: IEditorTabs) => void; 21 | } 22 | 23 | const EditorSetup: FC = ({ code, language, onChange }) => { 24 | return ( 25 | onChange(value, language)} 28 | style={{ height: "100%" }} 29 | highlight={code => ( 30 | 36 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 37 | 38 | {tokens.map((line, i) => ( 39 |
40 | {line.map((token, key) => ( 41 | 42 | ))} 43 |
44 | ))} 45 |
46 | )} 47 |
48 | )} 49 | padding={10} 50 | /> 51 | ); 52 | }; 53 | 54 | export default EditorSetup; 55 | -------------------------------------------------------------------------------- /playground/src/Draggable/useDrag.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, RefObject } from "react"; 2 | 3 | interface IProps { 4 | containerRef: RefObject; 5 | dividerRef: RefObject; 6 | dividerWidth: number; 7 | } 8 | 9 | function useDrag({ containerRef, dividerRef, dividerWidth }: IProps) { 10 | const [width, setWidth] = useState(0); 11 | const [containerRect, setContainerRect] = useState(null); 12 | 13 | useEffect(() => { 14 | const containerEl = containerRef.current; 15 | if (containerEl) { 16 | const fullWidth = containerEl.clientWidth; 17 | const containerRect = containerEl.getBoundingClientRect(); 18 | setContainerRect(containerRect); 19 | setWidth(fullWidth / 2); 20 | } 21 | }, []); 22 | const keepDragging = useCallback( 23 | (event: MouseEvent) => { 24 | const { clientX } = event; 25 | if (containerRect) { 26 | setWidth(clientX - containerRect.left); 27 | } 28 | }, 29 | [containerRect] 30 | ); 31 | const stopDrag = useCallback(() => { 32 | document.removeEventListener("mousemove", keepDragging); 33 | document.removeEventListener("mouseup", stopDrag); 34 | }, [keepDragging]); 35 | const startDrag = useCallback(() => { 36 | document.addEventListener("mousemove", keepDragging); 37 | document.addEventListener("mouseup", stopDrag); 38 | }, [keepDragging, stopDrag]); 39 | useEffect(() => { 40 | const dividerEl = dividerRef.current; 41 | if (dividerEl) { 42 | dividerEl.addEventListener("mousedown", startDrag); 43 | } 44 | return () => { 45 | if (dividerEl) { 46 | dividerEl.removeEventListener("mousedown", startDrag); 47 | } 48 | }; 49 | }, [startDrag]); 50 | 51 | return { 52 | leftWidth: width, 53 | rightWidth: containerRect ? containerRect.width - width - dividerWidth : 0, 54 | }; 55 | } 56 | 57 | export default useDrag; 58 | -------------------------------------------------------------------------------- /playground/src/Result/Frame.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo, useState, memo, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { ISnippet } from "../types"; 5 | import constructSnippet from "../utils/constructSnippet"; 6 | import ErrorDisplay from "./ErrorDisplay"; 7 | 8 | const Container = styled.div` 9 | position: relative; 10 | height: 100%; 11 | 12 | &::after { 13 | content: ""; 14 | display: inline-block; 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | z-index: 1; 19 | top: 0; 20 | left: 0; 21 | } 22 | `; 23 | 24 | interface IProps { 25 | id: string | number; 26 | snippet: ISnippet; 27 | transformJs: boolean; 28 | presets: string[]; 29 | } 30 | 31 | const Frame: FC = memo(({ id, snippet, transformJs, presets }) => { 32 | const [code, setCode] = useState(""); 33 | const [error, setError] = useState(null); 34 | 35 | useMemo(() => { 36 | try { 37 | const code = constructSnippet(snippet, id, transformJs, presets); 38 | setCode(code); 39 | setError(null); 40 | } catch (err) { 41 | setError(err.message); 42 | } 43 | }, [snippet, transformJs]); 44 | 45 | useEffect(() => { 46 | function waitForMessage() { 47 | if (typeof window !== "undefined") { 48 | window.addEventListener("message", (data) => { 49 | if ( 50 | data.data.source === `frame-${id}` && 51 | data.data.message.type === "error" 52 | ) { 53 | setError(data.data.message.data); 54 | } 55 | }); 56 | } 57 | } 58 | waitForMessage(); 59 | }, [id]); 60 | 61 | return ( 62 | 63 |