├── src ├── react-app-env.d.ts ├── vite-env.d.ts ├── icons │ ├── add.svg │ ├── cross.svg │ ├── direction.svg │ ├── download.svg │ └── cog.svg ├── components │ ├── toolbar │ │ ├── Settings.tsx │ │ ├── ScaleSelect.tsx │ │ ├── ShadowSelector.tsx │ │ ├── SelectionToolbar.tsx │ │ └── Toolbar.tsx │ ├── Dropzone.tsx │ ├── inputs │ │ └── ColorPicker.tsx │ └── Window.tsx ├── store │ ├── index.ts │ ├── reducer.ts │ └── windows.ts ├── index.tsx ├── index.css ├── Nav.tsx ├── hooks.ts ├── utils │ └── image.ts └── App.tsx ├── public ├── icon16.png ├── icon192.png ├── icon512.png └── robots.txt ├── .idea ├── php.xml ├── codeStyles │ └── codeStyleConfig.xml ├── vcs.xml ├── .gitignore ├── modules.xml ├── inspectionProfiles │ └── Project_Default.xml ├── deployment.xml ├── graphite.iml └── misc.xml ├── .prettierrc.js ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── index.html ├── README.md ├── LICENSE └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duiker101/graphite-shot/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duiker101/graphite-shot/HEAD/public/icon192.png -------------------------------------------------------------------------------- /public/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duiker101/graphite-shot/HEAD/public/icon512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 4, 4 | semi: true, 5 | useTabs: true, 6 | bracketSpacing: false, 7 | jsxBracketSameLine: true, 8 | }; 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vite"; 2 | import react from "@vitejs/plugin-react" 3 | import svgr from "vite-plugin-svgr" 4 | 5 | export default defineConfig({plugins: [react(), svgr()]}) -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /src/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/components/toolbar/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import {useDispatch} from "react-redux"; 4 | 5 | const Wrapper = styled.div``; 6 | 7 | export default () => { 8 | const dispatch = useDispatch(); 9 | return ; 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/direction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {Action, configureStore, ThunkAction} from "@reduxjs/toolkit"; 2 | 3 | import rootReducer, {RootState} from "./reducer"; 4 | 5 | const store = configureStore({ 6 | reducer: rootReducer, 7 | }); 8 | 9 | export type AppDispatch = typeof store.dispatch; 10 | 11 | export type AppThunk = ThunkAction>; 12 | 13 | export default store; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/store/reducer.ts: -------------------------------------------------------------------------------- 1 | import {combineReducers} from "@reduxjs/toolkit"; 2 | import windows from "./windows"; 3 | import {useSelector} from "react-redux"; 4 | 5 | const rootReducer = combineReducers({windows}); 6 | 7 | export type RootState = ReturnType; 8 | 9 | export default rootReducer; 10 | 11 | export function useRootState(selector: (state: RootState) => any) { 12 | return useSelector((s: RootState) => selector(s)); 13 | } 14 | -------------------------------------------------------------------------------- /.idea/deployment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {createRoot} from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | import {Provider} from "react-redux"; 7 | import store from "./store"; 8 | 9 | const container = document.getElementById("root"); 10 | const root = createRoot(container!); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 4 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 5 | "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background: #343434; 9 | padding: 0; 10 | margin: 0; 11 | color: whitesmoke; 12 | height: 100vh; 13 | } 14 | 15 | #root { 16 | min-width: min-content; 17 | min-height: 100vh; 18 | height: 100vh; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "downlevelIteration": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /.idea/graphite.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Graphite 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Nav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Wrapper = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | height: 50px; 9 | `; 10 | 11 | const Section = styled.div` 12 | margin: 0 1ch; 13 | color: #aaa; 14 | `; 15 | 16 | const Link = styled.a` 17 | color: teal; 18 | &:visited { 19 | color: teal; 20 | } 21 | `; 22 | 23 | export default () => { 24 | return ( 25 | 26 |
27 | Made By{" "} 28 | @Duiker101 29 |
30 |
31 | 32 | GitHub 33 | 34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/toolbar/ScaleSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {useDispatch} from "react-redux"; 3 | import {setWindowScaling, useSelectedWindow} from "../../store/windows"; 4 | 5 | export default () => { 6 | const dispatch = useDispatch(); 7 | const selected = useSelectedWindow(); 8 | 9 | const scales = [0.5, 1, 2]; 10 | 11 | const onScalingChange = (e: React.ChangeEvent) => { 12 | dispatch( 13 | setWindowScaling({ 14 | id: selected.id, 15 | scale: parseFloat(e.target.value), 16 | }) 17 | ); 18 | }; 19 | 20 | return ( 21 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Graphite Shot 2 | 3 | #### Motivation 4 | 5 | There are quite a few tools to create nice looking images from code. Some examples that inspired this tool are: 6 | - [Carbon](https://carbon.now.sh) 7 | - [Polacode](https://marketplace.visualstudio.com/items?itemName=pnp.polacode) 8 | - [Codeimg](https://codeimg.io/) 9 | 10 | But all of these require that you actually write the code in them. I wanted something where I could write my code in the editor and just add the fancy window to the screenshot. 11 | Graphite Shot lets you do just that. 12 | 13 | #### Features 14 | 15 | - Drop, Paste or use dialog to select your image. 16 | - Add windows horizontally and vertically 17 | - Automatically choose the window color based on the most common color of the image 18 | - Change window and background colors 19 | 20 | #### Dev 21 | 22 | React App with Typescript and Styled-Components. 23 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import {Dispatch, SetStateAction, useEffect, useState} from "react"; 2 | 3 | export const usePastedImage = (): [ 4 | string | undefined, 5 | Dispatch>, 6 | ] => { 7 | const [imageData, setImageData] = useState(); 8 | useEffect(() => { 9 | const listener = (e: Event) => { 10 | const {clipboardData: data} = e as ClipboardEvent; 11 | const items = data?.items || []; 12 | 13 | for (let item of items) { 14 | if (item.type.indexOf("image") === -1) continue; 15 | const blob = item.getAsFile() as Blob; 16 | let URLObj = window.URL || window.webkitURL; 17 | setImageData(URLObj.createObjectURL(blob)); 18 | return; 19 | } 20 | }; 21 | 22 | window.addEventListener("paste", listener); 23 | return () => window.removeEventListener("paste", listener); 24 | }, []); 25 | 26 | return [imageData, setImageData]; 27 | }; 28 | -------------------------------------------------------------------------------- /src/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/image.ts: -------------------------------------------------------------------------------- 1 | import {intToRGBA, Jimp} from "jimp"; 2 | 3 | export const processImage = (imageData: string) => { 4 | return Jimp.read(imageData).then((image) => { 5 | const colors: { [key: string]: number } = {}; 6 | 7 | for (let x = 0; x < image.width; x += 2) { 8 | for (let y = 0; y < image.height; y += 2) { 9 | const key = image.getPixelColor(x, y); 10 | colors[key] = colors[key] + 1 || 1; 11 | } 12 | } 13 | 14 | const color = Object.entries(colors).reduce( 15 | (a, [key, value]) => { 16 | if (value > a[1]) return [key, value]; 17 | return a; 18 | }, 19 | ["", 0] 20 | )[0]; 21 | 22 | const {r, g, b} = intToRGBA(parseInt(color)); 23 | const hex = [r, g, b] 24 | .map((n) => n.toString(16).padStart(2, "0")) 25 | .join(""); 26 | 27 | return {color: "#" + hex, image}; 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Simone Masiero 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. 22 | -------------------------------------------------------------------------------- /src/components/toolbar/ShadowSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import {useDispatch} from "react-redux"; 4 | import {Tooltip} from "react-tippy"; 5 | import {ShadowPicker} from "react-shadow-picker"; 6 | import {AppDispatch} from "../../store"; 7 | import {useSelectedWindow, setWindowShadow} from "../../store/windows"; 8 | 9 | const Button = styled.div` 10 | border-radius: 50%; 11 | border: 1px solid white; 12 | box-shadow: 1px 1px 3px 0px #000f; 13 | height: 20px; 14 | width: 20px; 15 | margin: 5px; 16 | user-select: none; 17 | color: white; 18 | cursor: pointer; 19 | box-sizing: border-box; 20 | padding: 3px; 21 | &:hover { 22 | background: rgba(200, 200, 200, 0.1); 23 | box-shadow: 1px 1px 3px 0px #ffff; 24 | } 25 | `; 26 | 27 | const TooltipBg = styled.div` 28 | padding: 0.3em; 29 | background: #2a2a2a; 30 | border: 1px solid white; 31 | `; 32 | 33 | export default () => { 34 | const dispatch: AppDispatch = useDispatch(); 35 | const selection = useSelectedWindow(); 36 | 37 | const update = (shadow: string) => { 38 | dispatch(setWindowShadow({id: selection.id, shadow})); 39 | }; 40 | 41 | return ( 42 | 48 | 49 | 50 | } 51 | > 52 | {/**/} 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/icons/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphite-shot", 3 | "version": "0.1.2", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "vite build", 9 | "checkfmt": "prettier -l \"**/*.{js,json,jsx,ts,tsx}\"", 10 | "fmt": "prettier --write \"**/*.{js,json,jsx,ts,tsx}\"" 11 | }, 12 | "devDependencies": { 13 | "@types/file-saver": "^2.0.7", 14 | "@types/node": "^24.3.0", 15 | "@types/react": "^19.1.10", 16 | "@types/react-color": "^3.0.13", 17 | "@types/react-dom": "^19.1.7", 18 | "@types/react-redux": "^7.1.34", 19 | "@types/styled-components": "^5.1.34", 20 | "@types/uuid": "^10.0.0", 21 | "@types/webpack-env": "^1.18.8", 22 | "@vitejs/plugin-react": "^5.0.0", 23 | "npm-check-updates": "^18.0.2", 24 | "prettier": "^3.6.2", 25 | "react-scripts": "5.0.1", 26 | "typescript": "^5.9.2", 27 | "vite": "^7.1.2", 28 | "vite-plugin-svgr": "^4.3.0" 29 | }, 30 | "dependencies": { 31 | "@reduxjs/toolkit": "^2.8.2", 32 | "file-saver": "^2.0.5", 33 | "html-to-image": "^1.11.13", 34 | "jimp": "^1.6.0", 35 | "react": "^19.1.1", 36 | "react-color": "^2.19.3", 37 | "react-dom": "^19.1.1", 38 | "react-dropzone": "^14.3.8", 39 | "react-is": "^19.1.1", 40 | "react-redux": "^9.2.0", 41 | "react-shadow-picker": "^1.1.0", 42 | "react-tippy": "^1.4.0", 43 | "redux": "^5.0.1", 44 | "styled-components": "^6.1.19", 45 | "uuid": "^11.1.0" 46 | }, 47 | "eslintConfig": { 48 | "extends": "react-app" 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/toolbar/SelectionToolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from "react"; 2 | import styled from "styled-components"; 3 | import {useDispatch} from "react-redux"; 4 | import ColorPicker from "../inputs/ColorPicker"; 5 | import { 6 | removeWindow, 7 | setWindowColor, 8 | useSelectedWindow, 9 | useWindows, 10 | } from "../../store/windows"; 11 | import ScalingSelect from "./ScaleSelect"; 12 | import CrossImg from "../../icons/cross.svg"; 13 | import ShadowSelector from "./ShadowSelector"; 14 | 15 | const Remove = styled.div` 16 | border: 1px solid transparent; 17 | border-radius: 4px; 18 | height: 20px; 19 | width: 20px; 20 | user-select: none; 21 | color: white; 22 | cursor: pointer; 23 | margin-left: 0.8ch; 24 | 25 | svg { 26 | height: 100%; 27 | width: 100%; 28 | } 29 | 30 | &:hover { 31 | background: rgba(200, 200, 200, 0.4); 32 | } 33 | `; 34 | 35 | export default () => { 36 | const dispatch = useDispatch(); 37 | const selection = useSelectedWindow(); 38 | const windows = useWindows(); 39 | 40 | const palette = useMemo(() => { 41 | const windowColors = [ 42 | ...new Set(Object.entries(windows).map(([id, w]) => w.color)), 43 | ]; 44 | return [ 45 | "#3D7BC7", 46 | "#17826D", 47 | "#F7EBD1", 48 | "#DFAC5D", 49 | "#44B87E", 50 | ...windowColors, 51 | ]; 52 | }, [windows]); 53 | 54 | return ( 55 | <> 56 | 59 | dispatch(setWindowColor({id: selection.id, color: c})) 60 | } 61 | palette={palette} 62 | /> 63 | 64 | 65 | 66 | 67 | {Object.values(windows).length > 1 && ( 68 | dispatch(removeWindow(selection.id))}> 69 | 70 | 71 | )} 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/Dropzone.tsx: -------------------------------------------------------------------------------- 1 | import React, {PropsWithChildren, useCallback} from "react"; 2 | import styled from "styled-components"; 3 | import {useDropzone} from "react-dropzone"; 4 | 5 | const Wrapper = styled.div` 6 | position: relative; 7 | flex-direction: column; 8 | display: flex; 9 | flex: 1; 10 | `; 11 | 12 | const Placeholder = styled.div` 13 | border: 1px dashed white; 14 | border-radius: 4px; 15 | flex: 1; 16 | align-items: center; 17 | justify-content: center; 18 | display: flex; 19 | padding: 32px; 20 | `; 21 | 22 | const Cover = styled.div` 23 | position: absolute; 24 | top: 0; 25 | bottom: 0; 26 | left: 0; 27 | right: 0; 28 | background: hsla(218, 50%, 50%, 0.5); 29 | `; 30 | 31 | interface Props { 32 | onImage: (image: string) => void; 33 | hasImage: boolean; 34 | } 35 | 36 | export default ({children, onImage, hasImage}: PropsWithChildren) => { 37 | const onDrop = useCallback( 38 | (acceptedFiles: any) => { 39 | for (let file of acceptedFiles) { 40 | const reader = new FileReader(); 41 | reader.addEventListener( 42 | "load", 43 | () => { 44 | onImage(reader.result as string); 45 | }, 46 | false 47 | ); 48 | reader.readAsDataURL(file); 49 | } 50 | }, 51 | [onImage] 52 | ); 53 | 54 | const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop}); 55 | 56 | return ( 57 | 58 | 59 | {children} 60 | {!hasImage && ( 61 | Drop or paste an image here. 62 | )} 63 | {isDragActive && } 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useState} from "react"; 2 | import styled from "styled-components"; 3 | import Toolbar from "./components/toolbar/Toolbar"; 4 | import Window from "./components/Window"; 5 | import {useWindows} from "./store/windows"; 6 | import Nav from "./Nav"; 7 | 8 | const Wrapper = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | min-height: 100vh; 12 | height: 100vh; 13 | `; 14 | 15 | const Main = styled.div` 16 | flex: 1; 17 | min-width: 200px; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | min-width: min-content; 23 | `; 24 | 25 | /** 26 | * This border element is needed so that it will not be saved in the image 27 | * and the result will not have rounded corners 28 | */ 29 | const Border = styled.div` 30 | border: 2px solid white; 31 | border-radius: 4px; 32 | `; 33 | 34 | const Content = styled.div<{bg: string; horizontal: boolean}>` 35 | margin: auto; 36 | padding: 10px; 37 | background: ${(p) => p.bg}; 38 | padding: 64px; 39 | display: grid; 40 | grid-auto-flow: ${({horizontal}) => (horizontal ? "column" : "row")}; 41 | grid-gap: 64px; 42 | align-items: center; 43 | justify-items: center; 44 | `; 45 | 46 | export default () => { 47 | const [bgColor, setBgColor] = useState("cadetblue"); 48 | const contentRef = useRef(null); 49 | const windows = useWindows(); 50 | const [horizontal, setIsHorizontal] = useState(true); 51 | 52 | return ( 53 | 54 |
55 |
56 | setIsHorizontal(d)} 58 | horizontal={horizontal} 59 | bgColor={bgColor} 60 | onBgColor={setBgColor} 61 | content={contentRef.current} 62 | /> 63 | 64 | 65 | 70 | {Object.entries(windows).map(([id, w]) => ( 71 | 72 | ))} 73 | 74 | 75 |
76 |
77 |