From 9099fa9bb895addb11ceb6ab0186324e32c470a6 Mon Sep 17 00:00:00 2001
From: Yannis Barlas <yannisbarlas@gmail.com>
Date: Thu, 17 Sep 2020 21:36:13 +0300
Subject: [PATCH] theming first pass

---
 wax-prosemirror-components/package.json       |  1 +
 .../src/components/ToolGroupComponent.js      |  1 -
 .../components/comments/ConnectedComment.js   |  2 +-
 wax-prosemirror-components/src/icons/icons.js |  2 +-
 .../src/ui/buttons/Dropdown.js                |  4 +-
 .../src/ui/buttons/MenuButton.js              | 21 ++---
 .../src/ui/comments/CommentItem.js            | 10 ++-
 .../src/ui/comments/DateParser.js             | 72 ++++++++++++++++++
 .../src/ui/tables/InsertTableTool.js          | 11 ++-
 wax-prosemirror-layouts/package.json          |  1 +
 .../src/layouts/EditorElements.js             | 23 +++---
 .../src/layouts/EditoriaLayout.js             | 76 ++++++-------------
 .../src/coko-theme/index.js                   | 26 ++++---
 yarn.lock                                     | 16 +++-
 14 files changed, 174 insertions(+), 92 deletions(-)
 create mode 100644 wax-prosemirror-components/src/ui/comments/DateParser.js

diff --git a/wax-prosemirror-components/package.json b/wax-prosemirror-components/package.json
index b8271eed3..cda67ab42 100644
--- a/wax-prosemirror-components/package.json
+++ b/wax-prosemirror-components/package.json
@@ -17,6 +17,7 @@
     "@fortawesome/free-solid-svg-icons": "^5.12.0",
     "@fortawesome/react-fontawesome": "^0.0.17",
     "lodash": "^4.17.4",
+    "moment": "^2.29.0",
     "prop-types": "^15.7.2",
     "prosemirror-model": "1.11.0",
     "prosemirror-state": "1.3.3",
diff --git a/wax-prosemirror-components/src/components/ToolGroupComponent.js b/wax-prosemirror-components/src/components/ToolGroupComponent.js
index e1f76123f..8fc6f640b 100644
--- a/wax-prosemirror-components/src/components/ToolGroupComponent.js
+++ b/wax-prosemirror-components/src/components/ToolGroupComponent.js
@@ -5,7 +5,6 @@ import styled from 'styled-components';
 import Dropdown from '../ui/buttons/Dropdown';
 
 const Wrapper = styled.div`
-  background: #fff;
   display: inline-flex;
   align-items: center;
   padding: 0 4px;
diff --git a/wax-prosemirror-components/src/components/comments/ConnectedComment.js b/wax-prosemirror-components/src/components/comments/ConnectedComment.js
index dea7c8b56..3a3e4e534 100644
--- a/wax-prosemirror-components/src/components/comments/ConnectedComment.js
+++ b/wax-prosemirror-components/src/components/comments/ConnectedComment.js
@@ -65,7 +65,7 @@ export default ({ comment, top, commentId, recalculateTops }) => {
     const obj = {
       content,
       displayName: user.username,
-      timestamp: Math.floor(Date.now() / 300000),
+      timestamp: Math.floor(Date.now()),
     };
 
     comment.attrs.conversation.push(obj);
diff --git a/wax-prosemirror-components/src/icons/icons.js b/wax-prosemirror-components/src/icons/icons.js
index c7f654f19..016facfe0 100644
--- a/wax-prosemirror-components/src/icons/icons.js
+++ b/wax-prosemirror-components/src/icons/icons.js
@@ -35,7 +35,7 @@ export default {
   check: <FontAwesomeIcon icon={faCheck} />,
   times: <FontAwesomeIcon icon={faTimes} />,
   commentBubble: ({ className }) => (
-    <Svg viewBox="0 0 24 24">
+    <Svg className={className} viewBox="0 0 24 24">
       <path d="M0 0h24v24H0z" fill="none" />
       <path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z" />
     </Svg>
diff --git a/wax-prosemirror-components/src/ui/buttons/Dropdown.js b/wax-prosemirror-components/src/ui/buttons/Dropdown.js
index 617302671..ffd67ef3f 100644
--- a/wax-prosemirror-components/src/ui/buttons/Dropdown.js
+++ b/wax-prosemirror-components/src/ui/buttons/Dropdown.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import styled from 'styled-components';
 
 import MenuButton from './MenuButton';
+import { grid } from '@pubsweet/ui-toolkit';
 
 // font size 0 reason: https://stackoverflow.com/a/19212391
 const Wrapper = styled.div`
@@ -11,9 +12,8 @@ const Wrapper = styled.div`
 `;
 
 const DropWrapper = styled.div`
-  margin-top: 4px;
+  margin-top: ${grid(1)};
   position: absolute;
-  z-index: 2;
 `;
 
 const Dropdown = props => {
diff --git a/wax-prosemirror-components/src/ui/buttons/MenuButton.js b/wax-prosemirror-components/src/ui/buttons/MenuButton.js
index 09feb60c9..7445f6af8 100644
--- a/wax-prosemirror-components/src/ui/buttons/MenuButton.js
+++ b/wax-prosemirror-components/src/ui/buttons/MenuButton.js
@@ -2,27 +2,26 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import styled, { css } from 'styled-components';
 
+import { th, override } from '@pubsweet/ui-toolkit';
+
 import Icon from './Icon';
 
 const activeStyles = css`
-  background: gray;
-  color: white;
+  background: ${th('colorPrimary')};
+  color: ${th('colorTextReverse')};
 
   > svg {
-    fill: white;
+    fill: ${th('colorTextReverse')};
   }
 
   &:hover {
-    background: gray;
+    background: ${th('colorPrimary')};
   }
 `;
 
 const disabledStyles = css`
   cursor: not-allowed;
-
-  > svg {
-    fill: gainsboro;
-  }
+  opacity: 0.4;
 
   &:hover {
     background: none;
@@ -37,21 +36,25 @@ const Wrapper = styled.button.attrs(props => ({
   border: none;
   border-radius: 2px;
   cursor: pointer;
+  font-family: ${th('fontInterface')};
   height: 28px;
   outline: none;
   padding: 2px;
   transition: all 0.1s ease-in;
+  color: ${th('colorText')};
 
   > svg {
     transition: all 0.1s ease-in;
   }
 
   &:hover {
-    background: gainsboro;
+    background: ${th('colorBackgroundHue')};
   }
 
   ${props => props.active && activeStyles}
   ${props => props.disabled && disabledStyles}
+
+  ${override('Wax.MenuButton')}
 `;
 
 const StyledIcon = styled(Icon)`
diff --git a/wax-prosemirror-components/src/ui/comments/CommentItem.js b/wax-prosemirror-components/src/ui/comments/CommentItem.js
index 7772a028e..8a668f4e2 100644
--- a/wax-prosemirror-components/src/ui/comments/CommentItem.js
+++ b/wax-prosemirror-components/src/ui/comments/CommentItem.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import styled from 'styled-components';
 
-// import { th } from '../_helpers'
+import DateParser from './DateParser';
 
 const Wrapper = styled.div``;
 
@@ -32,7 +32,13 @@ const CommentItem = props => {
     <Wrapper className={className}>
       <Head>
         <Name>{displayName}</Name>
-        <Timestamp>{timestamp}</Timestamp>
+        <Timestamp>
+          <DateParser timestamp={timestamp}>
+            {(timeStamp, timeAgo) => {
+              return `${timeAgo} ago`;
+            }}
+          </DateParser>
+        </Timestamp>
       </Head>
       <Content>{content}</Content>
     </Wrapper>
diff --git a/wax-prosemirror-components/src/ui/comments/DateParser.js b/wax-prosemirror-components/src/ui/comments/DateParser.js
new file mode 100644
index 000000000..7bd8716bd
--- /dev/null
+++ b/wax-prosemirror-components/src/ui/comments/DateParser.js
@@ -0,0 +1,72 @@
+import moment from 'moment';
+import propTypes from 'prop-types';
+
+const getDuration = timestamp => {
+  const today = moment();
+  const stamp = moment(timestamp);
+  return moment.duration(today.diff(stamp));
+};
+
+// const D = ({ children, timestamp, timeAgo }) => children(timestamp, timeAgo);
+
+// const DateParser = compose(
+//   setDisplayName('DateParser'),
+//   withHandlers({
+//     renderTimestamp: ({
+//       timestamp,
+//       dateFormat = 'DD.MM.YYYY',
+//       humanizeThreshold = 0,
+//     }) => () => {
+//       if (!timestamp) return '';
+//       const duration = getDuration(timestamp);
+
+//       if (duration.asDays() < humanizeThreshold) {
+//         return `${duration.humanize()} ago`;
+//       }
+//       return moment(timestamp).format(dateFormat);
+//     },
+//     renderTimeAgo: ({ timestamp }) => () => {
+//       if (!timestamp) return '';
+//       const duration = getDuration(timestamp);
+//       return duration.humanize();
+//     },
+//   }),
+//   withProps(({ renderTimestamp, renderTimeAgo }) => ({
+//     timeAgo: renderTimeAgo(),
+//     timestamp: renderTimestamp(),
+//   })),
+// )(D);
+
+const DateParser = props => {
+  const { children, timestamp, dateFormat, humanizeThreshold } = props;
+
+  const renderTimeAgo = () => {
+    if (!timestamp) return '';
+    const duration = getDuration(timestamp);
+    return duration.humanize();
+  };
+
+  const renderTimestamp = () => {
+    if (!timestamp) return '';
+    const duration = getDuration(timestamp);
+
+    if (duration.asDays() < humanizeThreshold) {
+      return `${duration.humanize()} ago`;
+    }
+    return moment(timestamp).format(dateFormat);
+  };
+
+  return children(renderTimestamp(), renderTimeAgo());
+};
+
+DateParser.propTypes = {
+  /** The date string. Can be any date parsable by momentjs. */
+  timestamp: propTypes.oneOfType([propTypes.string, propTypes.number, Date])
+    .isRequired,
+  /** Format of the rendered date. */
+  dateFormat: propTypes.string,
+  /** Humanize duration threshold */
+  humanizeThreshold: propTypes.number,
+};
+
+export default DateParser;
diff --git a/wax-prosemirror-components/src/ui/tables/InsertTableTool.js b/wax-prosemirror-components/src/ui/tables/InsertTableTool.js
index 73cc4eb6b..37168ed6d 100644
--- a/wax-prosemirror-components/src/ui/tables/InsertTableTool.js
+++ b/wax-prosemirror-components/src/ui/tables/InsertTableTool.js
@@ -17,6 +17,7 @@
 import React, { useEffect, useState } from 'react';
 import PropTypes from 'prop-types';
 import ReactDOM from 'react-dom';
+import { withTheme } from 'styled-components';
 
 const clamp = (min, val, max) => {
   if (val < min) {
@@ -77,19 +78,19 @@ const CELL_SIZE = 16;
 const MAX_SIZE = 20;
 
 const GridCell = props => {
-  const { x, y, selected } = props;
+  const { x, y, selected, theme } = props;
   const style = {
     left: x + 'px',
     top: y + 'px',
     width: CELL_SIZE + 'px',
     height: CELL_SIZE + 'px',
-    border: '1px solid gray',
+    border: `1px solid ${theme.colorBorder}`,
     boxSizing: 'border-box',
     position: 'absolute',
     zIndex: 2,
   };
 
-  if (selected) style.background = '#E2E2E2';
+  if (selected) style.background = theme.colorPrimary;
   return <div style={style} />;
 };
 
@@ -99,6 +100,8 @@ GridCell.propTypes = {
   selected: PropTypes.bool.isRequired,
 };
 
+const ThemedCell = withTheme(GridCell);
+
 const TableGridSizeEditor = props => {
   let _ex = 0;
   let _ey = 0;
@@ -202,7 +205,7 @@ const TableGridSizeEditor = props => {
       x += GUTTER_SIZE;
       const selected = ii < rows && jj < cols;
       cells.push(
-        <GridCell
+        <ThemedCell
           key={`${String(ii)}-${String(jj)}`}
           selected={selected}
           x={x}
diff --git a/wax-prosemirror-layouts/package.json b/wax-prosemirror-layouts/package.json
index e96e5abf1..62430b10f 100644
--- a/wax-prosemirror-layouts/package.json
+++ b/wax-prosemirror-layouts/package.json
@@ -13,6 +13,7 @@
     "build": "BABEL_ENV=production rollup -c"
   },
   "dependencies": {
+    "@pubsweet/ui-toolkit": "^2.3.1",
     "react-panelgroup": "^1.0.10",
     "styled-components": "^4.2.0",
     "wax-prosemirror-components": "^0.0.19",
diff --git a/wax-prosemirror-layouts/src/layouts/EditorElements.js b/wax-prosemirror-layouts/src/layouts/EditorElements.js
index 21a55e398..b830a1842 100644
--- a/wax-prosemirror-layouts/src/layouts/EditorElements.js
+++ b/wax-prosemirror-layouts/src/layouts/EditorElements.js
@@ -1,27 +1,32 @@
 import { css } from 'styled-components';
 
+import { th } from '@pubsweet/ui-toolkit';
+
 /* All styles regarding ProseMirror surface and elements */
 
 export default css`
   .ProseMirror {
+    background: white;
     counter-reset: footnote;
-    font-family: ${props => props.theme.fontReading};
+    font-family: ${th('fontWriting')};
+    color: ${th('colorText')};
+
+    p::selection,
+    p span::selection {
+      background-color: transparent;
+    }
+
     &:focus {
       outline: none;
     }
   }
 
   .ProseMirror .wax-selection-marker {
-    background: #0a78ff;
+    background-color: teal;
     color: white;
   }
 
   div[contenteditable='false'] {
-    -webkit-touch-callout: none;
-    -webkit-user-select: none;
-    -khtml-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
     user-select: none;
     pointer-events: none;
   }
@@ -180,9 +185,9 @@ export default css`
 
   span.deletion {
     text-decoration: line-through;
-    color: red;
+    color: ${th('colorError')};
     footnote {
-      background: red;
+      background: ${th('colorError')};
     }
   }
 
diff --git a/wax-prosemirror-layouts/src/layouts/EditoriaLayout.js b/wax-prosemirror-layouts/src/layouts/EditoriaLayout.js
index 2fcf38483..e8c87ea4b 100644
--- a/wax-prosemirror-layouts/src/layouts/EditoriaLayout.js
+++ b/wax-prosemirror-layouts/src/layouts/EditoriaLayout.js
@@ -6,57 +6,29 @@ import { componentPlugin } from 'wax-prosemirror-services';
 import { cokoTheme } from 'wax-prosemirror-themes';
 import { DocumentHelpers } from 'wax-prosemirror-utilities';
 import { WaxContext } from 'wax-prosemirror-core';
+
+import { grid, th } from '@pubsweet/ui-toolkit';
 import EditorElements from './EditorElements';
 
 const divider = css`
   .divider {
-    &:before {
-      content: 'Notes';
-      position: relative;
-      bottom: 14px;
-      background: white;
-      z-index: 999;
-      color: #a3a3a3;
-      font-weight: 600;
-      letter-spacing: 0.15em;
-    }
-    &:after {
-      color: #a3a3a3;
-      content: '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . '
-        '. . . . . . . . . . . . . . . . . . . . ';
-      float: left;
-      font-weight: 400;
-      white-space: nowrap;
-      width: 0;
-      position: relative;
-      bottom: 14px;
+    > div {
+      background: ${th('colorBorder')};
+      height: ${grid(1)};
+      max-height: ${grid(1)};
+
+      &:hover {
+        height: ${grid(2)};
+        max-height: ${grid(2)};
+      }
     }
   }
 `;
 
 const Wrapper = styled.div`
+  background: ${th('colorBackground')};
+  font-family: ${th('fontInterface')};
+
   display: flex;
   flex-direction: column;
 
@@ -74,18 +46,18 @@ const Main = styled.div`
 
 const TopMenu = styled.div`
   display: flex;
-  justify-content: center;
   min-height: 40px;
   user-select: none;
-  border-bottom: 2px solid #ecedf1;
+  border-bottom: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')};
 
   > div:not(:last-child) {
-    border-right: 1px solid #ecedf1;
+    border-right: ${th('borderWidth')} ${th('borderStyle')}
+      ${th('colorFurniture')};
   }
 `;
 
 const SideMenu = styled.div`
-  border-right: 1px solid #ecedf1;
+  border-right: ${th('borderWidth')} ${th('borderStyle')} ${th('colorBorder')};
   min-width: 250px;
   height: 100%;
 `;
@@ -98,7 +70,6 @@ const WaxSurfaceScroll = styled.div`
   overflow-y: auto;
   display: flex;
   box-sizing: border-box;
-  padding: 0 2px 2px 2px;
   height: 100%;
   width: 100%;
 
@@ -124,22 +95,23 @@ const CommentsContainer = styled.div`
 `;
 
 const NotesAreaContainer = styled.div`
+  background: ${th('colorBackgroundHue')};
   display: flex;
   flex-direction: row;
   width: 100%;
   height: 100%;
   overflow-y: scroll;
-  position: absolute;
+  padding-top: ${grid(1)};
 `;
 
 const NotesContainer = styled.div`
   counter-reset: footnote-view;
   display: flex;
   flex-direction: column;
-  padding: 0 0 10px 5px;
+  padding-left: ${grid(10)};
+  padding-bottom: ${grid(4)};
   height: 100%;
   width: 65%;
-  position: relative;
 `;
 
 let surfaceHeight = 700;
@@ -190,8 +162,8 @@ const EditoriaLayout = ({ editor }) => {
             <PanelGroup
               direction="column"
               panelWidths={[
-                { size: surfaceHeight, resize: 'stretch' },
-                { size: notesHeight, resize: 'stretch' },
+                { size: surfaceHeight, resize: 'dynamic' },
+                { size: notesHeight, resize: 'dynamic' },
               ]}
               onResizeEnd={onResizeEnd}
             >
diff --git a/wax-prosemirror-themes/src/coko-theme/index.js b/wax-prosemirror-themes/src/coko-theme/index.js
index 7f8345715..96d3fe6a0 100644
--- a/wax-prosemirror-themes/src/coko-theme/index.js
+++ b/wax-prosemirror-themes/src/coko-theme/index.js
@@ -2,18 +2,18 @@
 import 'typeface-fira-sans-condensed';
 import 'typeface-vollkorn';
 
-import { ButtonStyles } from './elements';
+import { css } from 'styled-components';
 
 const cokoTheme = {
   /* Colors */
   colorBackground: 'white',
-  colorPrimary: '#808080',
+  colorPrimary: 'teal',
   colorSecondary: '#E7E7E7',
   colorFurniture: '#CCC',
   colorBorder: '#AAA',
   colorBackgroundHue: '#F1F1F1',
   colorSuccess: '#008800',
-  colorError: '#FF2D1A',
+  colorError: 'indianred',
   colorText: '#111',
   colorTextReverse: '#FFF',
   colorTextPlaceholder: '#595959',
@@ -24,10 +24,10 @@ const cokoTheme = {
   /* Text variables */
 
   // fonts
-  fontInterface: "'Fira Sans Condensed'",
-  fontHeading: "'Fira Sans Condensed'",
-  fontReading: "'Vollkorn'",
-  fontWriting: "'Cokourier Prime Sans'",
+  fontInterface: 'Fira Sans Condensed',
+  fontHeading: 'Fira Sans Condensed',
+  fontReading: 'Vollkorn',
+  fontWriting: 'Vollkorn',
 
   // font sizes
   fontSizeBase: '16px',
@@ -50,7 +50,7 @@ const cokoTheme = {
   lineHeightHeading6: '24px',
 
   /* Spacing */
-  gridUnit: '8px',
+  gridUnit: '4px',
 
   /* Border */
   borderRadius: '0',
@@ -68,8 +68,14 @@ const cokoTheme = {
   breakpoints: [480, 768, 1000, 1272],
 
   cssOverrides: {
-    ui: {
-      ButtonStyles,
+    Wax: {
+      MenuButton: css`
+        /* color: magenta;
+
+        > svg {
+          fill: indianred;
+        } */
+      `,
     },
   },
 };
diff --git a/yarn.lock b/yarn.lock
index c80c33964..9235c2621 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1979,6 +1979,15 @@
     lodash "^4.17.4"
     styled-components "^4.1.1"
 
+"@pubsweet/ui-toolkit@^2.3.1":
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/@pubsweet/ui-toolkit/-/ui-toolkit-2.3.2.tgz#44b9b97b399027ef90bf0c30768475284f437fb4"
+  integrity sha512-VJ9zZI1uDOCicmuJV7uv98kK0J5hGpJKIeXpOSrgPgFw3dDFRjuuLa4W8L5aFa+FG42ZDZEj2CaOnfuZyB4HGA==
+  dependencies:
+    color "^3.0.0"
+    lodash "^4.17.4"
+    styled-components "^4.1.1"
+
 "@reach/router@^1.3.3":
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c"
@@ -12008,6 +12017,11 @@ modify-values@^1.0.0:
   resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
   integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
 
+moment@^2.29.0:
+  version "2.29.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
+  integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
+
 moment@^2.6.0:
   version "2.27.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
@@ -16755,7 +16769,7 @@ style-to-object@^0.2.1:
   dependencies:
     inline-style-parser "0.1.1"
 
-styled-components@4.2.0, styled-components@^4.1.1, styled-components@^4.2.0:
+styled-components@^4.1.1, styled-components@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.2.0.tgz#811fbbec4d64c7189f6c7482b9eb6fefa7fefef7"
   integrity sha512-L/LzkL3ZbBhqIVHdR7DbYujy4tqvTNRfc+4JWDCYyhTatI+8CRRQUmdaR0+ARl03DWsfKLhjewll5uNLrqrl4A==
-- 
GitLab