Browse Source

create download & clipboard image modal

AykutSarac 2 years ago
parent
commit
820ad3f651

+ 3 - 1
package.json

@@ -16,10 +16,12 @@
     "@sentry/nextjs": "^7.1.1",
     "allotment": "^1.14.2",
     "compress-json": "^2.0.1",
+    "html-to-image": "^1.9.0",
     "next": "^12.1.5",
     "next-transpile-modules": "^9.0.0",
     "parse-json": "^6.0.2",
     "react": "^18.0.0",
+    "react-color": "^2.19.3",
     "react-dom": "^18.0.0",
     "react-hot-toast": "^2.2.0",
     "react-icons": "^4.3.1",
@@ -27,7 +29,6 @@
     "react-twitter-embed": "^4.0.4",
     "react-zoom-pan-pinch": "^2.1.3",
     "reaflow": "^5.0.4",
-    "save-html-as-image": "^1.7.1",
     "styled-components": "^5.3.5",
     "usehooks-ts": "^2.5.2",
     "zustand": "^4.0.0-rc.4"
@@ -41,6 +42,7 @@
     "@types/node": "^17.0.30",
     "@types/parse-json": "^4.0.0",
     "@types/react": "18.0.5",
+    "@types/react-color": "^3.0.6",
     "@types/react-splitter-layout": "^3.0.2",
     "@types/styled-components": "^5.1.25",
     "babel-jest": "^28",

+ 13 - 3
src/components/Button/index.tsx

@@ -12,19 +12,23 @@ enum ButtonType {
 export interface ButtonProps
   extends React.ButtonHTMLAttributes<HTMLButtonElement> {
   status?: keyof typeof ButtonType;
+  block?: boolean;
 }
 
 function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
   return theme[ButtonType[status]];
 }
 
-const StyledButton = styled.button<{ status: keyof typeof ButtonType }>`
+const StyledButton = styled.button<{
+  status: keyof typeof ButtonType;
+  block: boolean;
+}>`
   display: block;
   background: ${({ status, theme }) => getButtonStatus(status, theme)};
   color: #ffffff;
   padding: 8px 16px;
   min-width: 60px;
-  width: fit-content;
+  width: ${({ block }) => (block ? "100%" : "fit-content")};
 
   :disabled {
     cursor: not-allowed;
@@ -46,10 +50,16 @@ const StyledButtonContent = styled.div`
 export const Button: React.FC<ButtonProps> = ({
   children,
   status,
+  block = false,
   ...props
 }) => {
   return (
-    <StyledButton type="button" status={status ?? "PRIMARY"} {...props}>
+    <StyledButton
+      type="button"
+      status={status ?? "PRIMARY"}
+      block={block}
+      {...props}
+    >
       <StyledButtonContent>{children}</StyledButtonContent>
     </StyledButton>
   );

+ 1 - 1
src/components/Graph/index.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { Canvas, EdgeData, ElkRoot, NodeData } from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
-import { getEdgeNodes } from "src/containers/LiveEditor/helpers";
+import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
 import useConfig from "src/hooks/store/useConfig";
 import shallow from "zustand/shallow";
 

+ 0 - 15
src/components/Image/index.tsx

@@ -1,15 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-const StyledImage = styled.img`
-  object-fit: contain;
-  height: auto;
-
-  @media only screen and (max-width: 768px) {
-    width: 100%;
-  }
-`;
-
-export const Image = ({ ...props }) => {
-  return <StyledImage {...props} />;
-};

+ 21 - 0
src/components/Input/index.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+import styled from "styled-components";
+
+const StyledInput = styled.input`
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+  outline: none;
+  border: none;
+  border-radius: 4px;
+  line-height: 32px;
+  padding: 12px 8px;
+  width: 100%;
+  margin-bottom: 10px;
+  height: 30px;
+`;
+
+type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
+
+export const Input: React.FC<InputProps> = (props) => (
+  <StyledInput {...props} />
+);

+ 0 - 0
src/containers/SearchInput/index.tsx → src/components/SearchInput/index.tsx


+ 4 - 4
src/components/Sidebar/index.tsx

@@ -21,12 +21,12 @@ import {
 
 import { Tooltip } from "src/components/Tooltip";
 import { useRouter } from "next/router";
-import { ImportModal } from "src/containers/ImportModal";
-import { ClearModal } from "src/containers/ClearModal";
-import { ShareModal } from "src/containers/ShareModal";
+import { ImportModal } from "src/containers/Modals/ImportModal";
+import { ClearModal } from "src/containers/Modals/ClearModal";
+import { ShareModal } from "src/containers/Modals/ShareModal";
 import { IoAlertCircleSharp } from "react-icons/io5";
 import useConfig from "src/hooks/store/useConfig";
-import { getNextLayout } from "src/containers/LiveEditor/helpers";
+import { getNextLayout } from "src/containers/Editor/LiveEditor/helpers";
 
 const StyledSidebar = styled.div`
   display: flex;

+ 0 - 0
src/containers/JsonEditor/index.tsx → src/containers/Editor/JsonEditor/index.tsx


+ 0 - 0
src/containers/LiveEditor/helpers.ts → src/containers/Editor/LiveEditor/helpers.ts


+ 0 - 0
src/containers/LiveEditor/index.tsx → src/containers/Editor/LiveEditor/index.tsx


+ 2 - 2
src/containers/Editor/Panes.tsx

@@ -1,11 +1,11 @@
 import { Allotment } from "allotment";
 import React from "react";
-import { JsonEditor } from "src/containers/JsonEditor";
+import { JsonEditor } from "src/containers/Editor/JsonEditor";
 import { StyledEditor } from "./styles";
 import dynamic from "next/dynamic";
 import useConfig from "src/hooks/store/useConfig";
 
-const LiveEditor = dynamic(() => import("src/containers/LiveEditor"), {
+const LiveEditor = dynamic(() => import("src/containers/Editor/LiveEditor"), {
   ssr: false,
 });
 

+ 11 - 10
src/containers/Editor/Tools.tsx

@@ -1,5 +1,4 @@
 import React from "react";
-import { saveAsPng } from "save-html-as-image";
 import {
   AiOutlineFullscreen,
   AiOutlineMinus,
@@ -8,10 +7,11 @@ import {
 import { FiDownload } from "react-icons/fi";
 import { HiOutlineSun, HiOutlineMoon } from "react-icons/hi";
 import { MdCenterFocusWeak } from "react-icons/md";
-import { SearchInput } from "src/containers/SearchInput";
+import { SearchInput } from "src/components/SearchInput";
 import styled from "styled-components";
 import useConfig from "src/hooks/store/useConfig";
 import shallow from "zustand/shallow";
+import { DownloadModal } from "../Modals/DownloadModal";
 
 export const StyledTools = styled.div`
   display: flex;
@@ -41,6 +41,7 @@ const StyledToolElement = styled.button`
 `;
 
 export const Tools: React.FC = () => {
+  const [isDownloadVisible, setDownloadVisible] = React.useState(false);
   const [lightmode, performance, hideEditor] = useConfig(
     (state) => [
       state.settings.lightmode,
@@ -58,13 +59,6 @@ export const Tools: React.FC = () => {
   const toggleEditor = () => updateSetting("hideEditor", !hideEditor);
   const toggleTheme = () => updateSetting("lightmode", !lightmode);
 
-  const exportAsImage = () => {
-    saveAsPng(document.querySelector("svg[id*='ref']"), {
-      filename: "jsonvisio.com",
-      printDate: true,
-    });
-  };
-
   return (
     <StyledTools>
       <StyledToolElement aria-label="fullscreen" onClick={toggleEditor}>
@@ -75,7 +69,10 @@ export const Tools: React.FC = () => {
       </StyledToolElement>
       {!performance && <SearchInput />}
       {!performance && (
-        <StyledToolElement aria-label="save" onClick={exportAsImage}>
+        <StyledToolElement
+          aria-label="save"
+          onClick={() => setDownloadVisible(true)}
+        >
           <FiDownload />
         </StyledToolElement>
       )}
@@ -88,6 +85,10 @@ export const Tools: React.FC = () => {
       <StyledToolElement aria-label="zoom in" onClick={zoomIn}>
         <AiOutlinePlus />
       </StyledToolElement>
+      <DownloadModal
+        visible={isDownloadVisible}
+        setVisible={setDownloadVisible}
+      />
     </StyledTools>
   );
 };

+ 0 - 0
src/containers/ClearModal/index.tsx → src/containers/Modals/ClearModal/index.tsx


+ 202 - 0
src/containers/Modals/DownloadModal/index.tsx

@@ -0,0 +1,202 @@
+import React from "react";
+import { FiCopy, FiDownload } from "react-icons/fi";
+import { toBlob, toPng } from "html-to-image";
+import { Button } from "src/components/Button";
+import { Input } from "src/components/Input";
+import { Modal, ModalProps } from "src/components/Modal";
+import { TwitterPicker } from "react-color";
+import { TwitterPickerStylesProps } from "react-color/lib/components/twitter/Twitter";
+import styled from "styled-components";
+import toast from "react-hot-toast";
+
+const ColorPickerStyles: Partial<TwitterPickerStylesProps> = {
+  card: {
+    background: "transparent",
+    boxShadow: "none",
+  },
+  body: {
+    padding: 0,
+  },
+  input: {
+    background: "rgba(0, 0, 0, 0.2)",
+    boxShadow: "none",
+    textTransform: "none",
+    whiteSpace: "nowrap",
+    textOverflow: "ellipsis",
+  },
+  hash: {
+    background: "rgba(180, 180, 180, 0.3)",
+  },
+};
+
+const defaultColors = [
+  "#B80000",
+  "#DB3E00",
+  "#FCCB00",
+  "#008B02",
+  "#006B76",
+  "#1273DE",
+  "#004DCF",
+  "#5300EB",
+  "#EB9694",
+  "#FAD0C3",
+  "#FEF3BD",
+  "#C1E1C5",
+  "#BEDADC",
+  "#C4DEF6",
+  "#BED3F3",
+  "#D4C4FB",
+  "transparent",
+];
+
+function downloadURI(uri: string, name: string) {
+  var link = document.createElement("a");
+  link.download = name;
+  link.href = uri;
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+}
+
+const StyledContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  padding: 12px 0;
+  border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  font-size: 12px;
+  line-height: 16px;
+  font-weight: 600;
+  text-transform: uppercase;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+
+  &:first-of-type {
+    padding-top: 0;
+    border: none;
+  }
+`;
+
+const StyledColorWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
+
+const StyledColorIndicator = styled.div<{ color: string }>`
+  flex: 1;
+  width: 100%;
+  height: auto;
+  border-radius: 6px;
+  background: ${({ color }) => color};
+  border: 1px solid;
+  border-color: rgba(0, 0, 0, 0.1);
+`;
+
+export const DownloadModal: React.FC<ModalProps> = ({
+  visible,
+  setVisible,
+}) => {
+  const [fileDetails, setFileDetails] = React.useState({
+    filename: "jsonvisio.com",
+    backgroundColor: "transparent",
+    quality: 1,
+  });
+
+  const clipboardImage = async () => {
+    try {
+      toast.loading("Copying to clipboard...", { id: "toastClipboard" });
+
+      const imageElement = document.querySelector(
+        "svg[id*='ref']"
+      ) as HTMLElement;
+
+      const blob = await toBlob(imageElement, {
+        quality: fileDetails.quality,
+        backgroundColor: fileDetails.backgroundColor,
+      });
+
+      if (!blob) return;
+
+      navigator.clipboard.write([
+        new ClipboardItem({
+          [blob.type]: blob,
+        }),
+      ]);
+
+      toast.success("Copied to clipboard");
+    } catch (error) {
+      toast.error("Failed to copy to clipboard");
+    } finally {
+      toast.dismiss("toastClipboard");
+      setVisible(false);
+    }
+  };
+
+  const exportAsImage = async () => {
+    try {
+      toast.loading("Downloading...", { id: "toastDownload" });
+
+      const imageElement = document.querySelector(
+        "svg[id*='ref']"
+      ) as HTMLElement;
+
+      const dataURI = await toPng(imageElement, {
+        quality: fileDetails.quality,
+        backgroundColor: fileDetails.backgroundColor,
+      });
+
+      downloadURI(dataURI, `${fileDetails.filename}.png`);
+    } catch (error) {
+      toast.error("Failed to download image!");
+    } finally {
+      toast.dismiss("toastDownload");
+      setVisible(false);
+    }
+  };
+
+  const updateDetails = (
+    key: keyof typeof fileDetails,
+    value: string | number
+  ) => setFileDetails({ ...fileDetails, [key]: value });
+
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Download Image</Modal.Header>
+      <Modal.Content>
+        <StyledContainer>
+          File Name
+          <StyledColorWrapper>
+            <Input
+              placeholder="File Name"
+              value={fileDetails.filename}
+              onChange={(e) => updateDetails("filename", e.target.value)}
+            />
+          </StyledColorWrapper>
+        </StyledContainer>
+        <StyledContainer>
+          Background Color
+          <StyledColorWrapper>
+            <TwitterPicker
+              triangle="hide"
+              colors={defaultColors}
+              color={fileDetails.backgroundColor}
+              onChange={(color) => updateDetails("backgroundColor", color.hex)}
+              styles={{
+                default: ColorPickerStyles,
+              }}
+            />
+            <StyledColorIndicator color={fileDetails.backgroundColor} />
+          </StyledColorWrapper>
+        </StyledContainer>
+      </Modal.Content>
+      <Modal.Controls setVisible={setVisible}>
+        <Button status="SECONDARY" onClick={clipboardImage}>
+          <FiCopy size={18} /> Clipboard
+        </Button>
+        <Button status="SUCCESS" onClick={exportAsImage}>
+          <FiDownload size={18} />
+          Download
+        </Button>
+      </Modal.Controls>
+    </Modal>
+  );
+};

+ 2 - 14
src/containers/ImportModal/index.tsx → src/containers/Modals/ImportModal/index.tsx

@@ -4,22 +4,10 @@ import toast from "react-hot-toast";
 
 import { Modal, ModalProps } from "src/components/Modal";
 import { Button } from "src/components/Button";
+import { Input } from "src/components/Input";
 import { AiOutlineUpload } from "react-icons/ai";
 import useConfig from "src/hooks/store/useConfig";
 
-const StyledInput = styled.input`
-  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-  outline: none;
-  border: none;
-  border-radius: 4px;
-  line-height: 32px;
-  padding: 12px 8px;
-  width: 100%;
-  margin-bottom: 10px;
-  height: 30px;
-`;
-
 const StyledModalContent = styled(Modal.Content)`
   display: flex;
   justify-content: center;
@@ -99,7 +87,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
     <Modal visible={visible} setVisible={setVisible}>
       <Modal.Header>Import JSON</Modal.Header>
       <StyledModalContent>
-        <StyledInput
+        <Input
           value={url}
           onChange={(e) => setURL(e.target.value)}
           type="url"

+ 2 - 14
src/containers/ShareModal/index.tsx → src/containers/Modals/ShareModal/index.tsx

@@ -7,19 +7,7 @@ import { Button } from "src/components/Button";
 import { BiErrorAlt } from "react-icons/bi";
 import { compress } from "compress-json";
 import useConfig from "src/hooks/store/useConfig";
-
-const StyledInput = styled.input`
-  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-  outline: none;
-  border: none;
-  border-radius: 4px;
-  line-height: 32px;
-  padding: 12px 8px;
-  width: 100%;
-  margin-bottom: 10px;
-  height: 30px;
-`;
+import { Input } from "src/components/Input";
 
 const StyledWarning = styled.p``;
 
@@ -65,7 +53,7 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
             </StyledWarning>
           </StyledErrorWrapper>
         ) : (
-          <StyledInput value={url} type="url" readOnly />
+          <Input value={url} type="url" readOnly />
         )}
       </Modal.Content>
       <Modal.Controls setVisible={setVisible}>

+ 1 - 2
src/pages/404.tsx

@@ -3,7 +3,6 @@ import { useRouter } from "next/router";
 import styled from "styled-components";
 
 import { Button } from "src/components/Button";
-import { Image } from "src/components/Image";
 
 const StyledNotFound = styled.div`
   display: flex;
@@ -37,7 +36,7 @@ const NotFound: React.FC = () => {
   return (
     <StyledNotFound>
       <StyledImageWrapper>
-        <Image src="/404.svg" alt="404" width={300} height={400} />
+        <img src="/404.svg" alt="not found" width={300} height={400} />
       </StyledImageWrapper>
       <StyledMessage>WIZARDS BEHIND CURTAINS?</StyledMessage>
       <StyledSubMessage>

+ 2 - 1
src/pages/_document.tsx

@@ -52,8 +52,9 @@ class MyDocument extends Document {
             crossOrigin="anonymous"
           />
           <link
-            href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700;900&family=Roboto+Mono:wght@500&family=Catamaran:wght@400;500;600&display=swap"
+            href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700;900&family=Roboto+Mono:wght@500&family=Catamaran:wght@400;500;600;700&display=swap"
             rel="stylesheet"
+            crossOrigin="anonymous"
           />
         </Head>
         <body>

+ 57 - 21
yarn.lock

@@ -1006,6 +1006,11 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@icons/material@^0.2.4":
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+  integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1806,6 +1811,14 @@
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
   integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
 
+"@types/react-color@^3.0.6":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a"
+  integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==
+  dependencies:
+    "@types/react" "*"
+    "@types/reactcss" "*"
+
 "@types/react-dom@^18.0.0":
   version "18.0.3"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831"
@@ -1838,6 +1851,13 @@
     "@types/scheduler" "*"
     csstype "^3.0.2"
 
+"@types/reactcss@*":
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc"
+  integrity sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==
+  dependencies:
+    "@types/react" "*"
+
 "@types/[email protected]":
   version "1.17.1"
   resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -3499,11 +3519,6 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
-file-saver@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
-  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
-
 filelist@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83"
@@ -4889,6 +4904,11 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash-es@^4.17.15:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.clamp@^4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa"
@@ -4924,7 +4944,7 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
-lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20:
+lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -4979,6 +4999,11 @@ [email protected]:
   dependencies:
     tmpl "1.0.5"
 
+material-colors@^1.2.1:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+  integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -5557,7 +5582,7 @@ prompts@^2.0.1:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
-prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -5622,6 +5647,19 @@ rdk@^6.0.1:
     popper.js "^1.16.1"
     react-scrolllock "^5.0.1"
 
+react-color@^2.19.3:
+  version "2.19.3"
+  resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
+  integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
+  dependencies:
+    "@icons/material" "^0.2.4"
+    lodash "^4.17.15"
+    lodash-es "^4.17.15"
+    material-colors "^1.2.1"
+    prop-types "^15.5.10"
+    reactcss "^1.2.0"
+    tinycolor2 "^1.4.1"
+
 react-cool-dimensions@^2.0.7:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/react-cool-dimensions/-/react-cool-dimensions-2.0.7.tgz#2fe6657608f034cd7c89f149ed14e79cf1cb2d50"
@@ -5703,6 +5741,13 @@ react@^18.0.0:
   dependencies:
     loose-envify "^1.1.0"
 
+reactcss@^1.2.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+  integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+  dependencies:
+    lodash "^4.0.1"
+
 readable-stream@^2.0.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -5941,20 +5986,6 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-save-html-as-image@^1.7.1:
-  version "1.7.1"
-  resolved "https://registry.yarnpkg.com/save-html-as-image/-/save-html-as-image-1.7.1.tgz#6fa291e45a0308f1837ea90fd0b46c0ff5758501"
-  integrity sha512-9pM9ljvEppzrnGmiB+BmbzV4uncI84rfG+NEDK6CLyTvMAt6ANBIzEkwJsbKbsV09hB2Qpn6Lp4bvTrFYFWadg==
-  dependencies:
-    file-saver "^2.0.5"
-    html-to-image "^1.9.0"
-    save-svg-as-png "^1.4.17"
-
-save-svg-as-png@^1.4.17:
-  version "1.4.17"
-  resolved "https://registry.yarnpkg.com/save-svg-as-png/-/save-svg-as-png-1.4.17.tgz#294442002772a24f1db1bf8a2aaf7df4ab0cdc55"
-  integrity sha512-7QDaqJsVhdFPwviCxkgHiGm9omeaMBe1VKbHySWU6oFB2LtnGCcYS13eVoslUgq6VZC6Tjq/HddBd1K6p2PGpA==
-
 saxes@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@@ -6433,6 +6464,11 @@ through@~2.3.4, through@~2.3.8:
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
+tinycolor2@^1.4.1:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
 [email protected]:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"