@squiz/formatted-text-editor 1.34.1-alpha.7 → 1.34.1-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
  2. package/lib/EditorToolbar/Tools/Link/LinkButton.js +1 -1
  3. package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.d.ts +2 -0
  4. package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.js +22 -0
  5. package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +3 -2
  6. package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +7 -1
  7. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +5 -0
  8. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +30 -0
  9. package/lib/Extensions/Extensions.d.ts +1 -0
  10. package/lib/Extensions/Extensions.js +3 -0
  11. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +2 -0
  12. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +23 -0
  13. package/lib/index.css +41 -0
  14. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +2 -1
  15. package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +6 -4
  16. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +7 -2
  17. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +7 -0
  18. package/package.json +4 -4
  19. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +3 -2
  20. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +3 -2
  21. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.spec.tsx +47 -0
  22. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +32 -0
  23. package/src/EditorToolbar/Tools/TextType/Heading/HeadingButton.spec.tsx +2 -2
  24. package/src/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.spec.tsx +1 -1
  25. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.spec.tsx +2 -2
  26. package/src/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.tsx +3 -1
  27. package/src/EditorToolbar/Tools/TextType/TextTypeDropdown.tsx +11 -1
  28. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +34 -0
  29. package/src/Extensions/Extensions.ts +3 -0
  30. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +3 -1
  31. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +31 -0
  32. package/src/index.scss +3 -0
  33. package/src/ui/ToolbarDropdownButton/ToolbarDropdownButton.tsx +8 -4
  34. package/src/ui/ToolbarDropdownButton/_toolbar-dropdown-button.scss +11 -0
  35. package/src/ui/_typography.scss +26 -0
  36. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +37 -0
  37. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -2
  38. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +39 -2
  39. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +6 -0
  40. package/src/utils/getNodeNamesByGroup.spec.ts +1 -1
  41. package/tailwind.config.cjs +4 -3
@@ -38,7 +38,7 @@ const ImageButton = ({ inPopover = false }) => {
38
38
  const active = (0, react_2.useActive)();
39
39
  const selection = (0, react_2.useCurrentSelection)();
40
40
  // if the active selection is not an image, disable the button as it means it will be text
41
- const disabled = !selection.empty && !active.image() && !active.assetImage();
41
+ const disabled = (!selection.empty && !active.image() && !active.assetImage()) || active.codeBlock();
42
42
  const handleClick = () => {
43
43
  if (!showModal) {
44
44
  setShowModal(true);
@@ -37,7 +37,7 @@ const LinkButton = ({ inPopover = false }) => {
37
37
  const { updateLink, updateAssetLink } = (0, react_2.useCommands)();
38
38
  const active = (0, react_2.useActive)();
39
39
  // If the image tool is active, disable the link tool as they shouldn't work at the same time
40
- const disabled = active.image();
40
+ const disabled = active.image() || active.codeBlock();
41
41
  const handleClick = () => {
42
42
  if (!showModal) {
43
43
  setShowModal(true);
@@ -0,0 +1,2 @@
1
+ declare const CodeBlockButton: () => JSX.Element;
2
+ export default CodeBlockButton;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ const react_2 = require("@remirror/react");
8
+ const ToolbarDropdownButton_1 = __importDefault(require("../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton"));
9
+ const CodeRounded_1 = __importDefault(require("@mui/icons-material/CodeRounded"));
10
+ const CodeBlockButton = () => {
11
+ const { toggleCodeBlock } = (0, react_2.useCommands)();
12
+ const active = (0, react_2.useActive)();
13
+ const enabled = toggleCodeBlock.enabled();
14
+ const handleSelect = () => {
15
+ if (toggleCodeBlock.enabled()) {
16
+ toggleCodeBlock();
17
+ }
18
+ };
19
+ return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.codeBlock(), label: "Code block", icon: react_1.default.createElement(CodeRounded_1.default, null) },
20
+ react_1.default.createElement("p", null, "Code block")));
21
+ };
22
+ exports.default = CodeBlockButton;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
7
  const react_2 = require("@remirror/react");
8
8
  const ToolbarDropdownButton_1 = __importDefault(require("../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton"));
9
+ const ShortTextRounded_1 = __importDefault(require("@mui/icons-material/ShortTextRounded"));
9
10
  const PreformattedButton = () => {
10
11
  const { togglePreformatted } = (0, react_2.useCommands)();
11
12
  const active = (0, react_2.useActive)();
@@ -15,7 +16,7 @@ const PreformattedButton = () => {
15
16
  togglePreformatted();
16
17
  }
17
18
  };
18
- return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.preformatted(), label: "Preformatted" },
19
- react_1.default.createElement("pre", null, "Preformatted")));
19
+ return (react_1.default.createElement(ToolbarDropdownButton_1.default, { handleOnClick: handleSelect, isDisabled: !enabled, isActive: active.preformatted(), label: "Preformatted", icon: react_1.default.createElement(ShortTextRounded_1.default, null) },
20
+ react_1.default.createElement("p", null, "Preformatted")));
20
21
  };
21
22
  exports.default = PreformattedButton;
@@ -7,6 +7,7 @@ const react_1 = __importDefault(require("react"));
7
7
  const HeadingButton_1 = __importDefault(require("./Heading/HeadingButton"));
8
8
  const ParagraphButton_1 = __importDefault(require("./Paragraph/ParagraphButton"));
9
9
  const PreformattedButton_1 = __importDefault(require("./Preformatted/PreformattedButton"));
10
+ const CodeBlockButton_1 = __importDefault(require("./CodeBlock/CodeBlockButton"));
10
11
  const ToolbarDropdown_1 = __importDefault(require("../../../ui/ToolbarDropdown/ToolbarDropdown"));
11
12
  const react_2 = require("@remirror/react");
12
13
  const TextTypeDropdown = () => {
@@ -16,6 +17,10 @@ const TextTypeDropdown = () => {
16
17
  if (active.preformatted()) {
17
18
  return 'Preformatted';
18
19
  }
20
+ // Determine if codeblock is active
21
+ if (active.codeBlock()) {
22
+ return 'Code block';
23
+ }
19
24
  // Determine if a heading is active
20
25
  for (let i = 1; i <= 6; i++) {
21
26
  if (active.heading({ level: i })) {
@@ -34,7 +39,8 @@ const TextTypeDropdown = () => {
34
39
  react_1.default.createElement(HeadingButton_1.default, { level: 4 }),
35
40
  react_1.default.createElement(HeadingButton_1.default, { level: 5 }),
36
41
  react_1.default.createElement(HeadingButton_1.default, { level: 6 }),
37
- react_1.default.createElement(PreformattedButton_1.default, null)),
42
+ react_1.default.createElement(PreformattedButton_1.default, null),
43
+ react_1.default.createElement(CodeBlockButton_1.default, null)),
38
44
  react_1.default.createElement(react_2.VerticalDivider, null)));
39
45
  };
40
46
  exports.default = TextTypeDropdown;
@@ -0,0 +1,5 @@
1
+ import { CodeBlockExtension } from '@remirror/extension-code-block';
2
+ import { NodeViewMethod } from 'remirror';
3
+ export declare class ExtendedCodeBlockExtension extends CodeBlockExtension {
4
+ createNodeViews(): NodeViewMethod;
5
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExtendedCodeBlockExtension = void 0;
4
+ const extension_code_block_1 = require("@remirror/extension-code-block");
5
+ class ExtendedCodeBlockExtension extends extension_code_block_1.CodeBlockExtension {
6
+ createNodeViews() {
7
+ return (node) => {
8
+ const { language } = node.attrs;
9
+ // This is the pre container for the code block
10
+ const dom = document.createElement('pre');
11
+ dom.setAttribute('spellcheck', 'false');
12
+ dom.classList.add(`code-block`);
13
+ // This is the actual code content in the code block
14
+ const contentDOM = document.createElement('code');
15
+ contentDOM.setAttribute('data-code-block-language', language);
16
+ // Divider between code block and pre container
17
+ const dividerElement = document.createElement('div');
18
+ dividerElement.classList.add('block-divider');
19
+ // The material icon to use
20
+ const codeIcon = document.createElement('svg');
21
+ codeIcon.classList.add('material-symbols-rounded');
22
+ codeIcon.textContent = 'code';
23
+ dom.append(codeIcon);
24
+ dom.append(dividerElement);
25
+ dom.append(contentDOM);
26
+ return { dom, contentDOM };
27
+ };
28
+ }
29
+ }
30
+ exports.ExtendedCodeBlockExtension = ExtendedCodeBlockExtension;
@@ -2,6 +2,7 @@ import { Extension } from '@remirror/core';
2
2
  import { EditorContextOptions } from '../Editor/EditorContext';
3
3
  export declare enum NodeName {
4
4
  Image = "image",
5
+ CodeBlock = "codeBlock",
5
6
  AssetImage = "assetImage",
6
7
  Text = "text"
7
8
  }
@@ -8,10 +8,12 @@ const LinkExtension_1 = require("./LinkExtension/LinkExtension");
8
8
  const ImageExtension_1 = require("./ImageExtension/ImageExtension");
9
9
  const CommandsExtension_1 = require("./CommandsExtension/CommandsExtension");
10
10
  const AssetImageExtension_1 = require("./ImageExtension/AssetImageExtension");
11
+ const CodeBlockExtension_1 = require("./CodeBlockExtension/CodeBlockExtension");
11
12
  const ClearFormattingExtension_1 = require("./ClearFormattingExtension/ClearFormattingExtension");
12
13
  var NodeName;
13
14
  (function (NodeName) {
14
15
  NodeName["Image"] = "image";
16
+ NodeName["CodeBlock"] = "codeBlock";
15
17
  NodeName["AssetImage"] = "assetImage";
16
18
  NodeName["Text"] = "text";
17
19
  })(NodeName = exports.NodeName || (exports.NodeName = {}));
@@ -30,6 +32,7 @@ const createExtensions = (context) => {
30
32
  new extensions_1.NodeFormattingExtension({ indents: [] }),
31
33
  new extensions_1.ParagraphExtension(),
32
34
  new PreformattedExtension_1.PreformattedExtension(),
35
+ new CodeBlockExtension_1.ExtendedCodeBlockExtension({ defaultWrap: true }),
33
36
  new extensions_1.UnderlineExtension(),
34
37
  new extensions_1.HistoryExtension(),
35
38
  new ImageExtension_1.ImageExtension(),
@@ -1,8 +1,10 @@
1
1
  import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
2
+ import { NodeViewMethod } from 'remirror';
2
3
  export declare class PreformattedExtension extends NodeExtension {
3
4
  get name(): "preformatted";
4
5
  createTags(): ("block" | "formattingNode" | "textBlock")[];
5
6
  createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
7
+ createNodeViews(): NodeViewMethod;
6
8
  /**
7
9
  * Toggle the <pre> for the current block.
8
10
  */
@@ -34,6 +34,29 @@ let PreformattedExtension = class PreformattedExtension extends core_1.NodeExten
34
34
  },
35
35
  };
36
36
  }
37
+ createNodeViews() {
38
+ return (node) => {
39
+ const { nodeTextAlignment } = node.attrs;
40
+ // This is the pre container for the code block
41
+ const dom = document.createElement('div');
42
+ dom.classList.add(`preformatted`);
43
+ // This is the actual code content in the code block
44
+ const contentDOM = document.createElement('pre');
45
+ contentDOM.setAttribute('data-node-text-align', nodeTextAlignment);
46
+ contentDOM.setAttribute('style', `text-align:${nodeTextAlignment}`);
47
+ // Divider between code block and pre container
48
+ const dividerElement = document.createElement('div');
49
+ dividerElement.classList.add('block-divider');
50
+ // The material icon to use
51
+ const codeIcon = document.createElement('svg');
52
+ codeIcon.classList.add('material-symbols-rounded');
53
+ codeIcon.textContent = 'short_text';
54
+ dom.append(codeIcon);
55
+ dom.append(dividerElement);
56
+ dom.append(contentDOM);
57
+ return { dom, contentDOM };
58
+ };
59
+ }
37
60
  /**
38
61
  * Toggle the <pre> for the current block.
39
62
  */
package/lib/index.css CHANGED
@@ -1,3 +1,5 @@
1
+ @import "https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded";
2
+
1
3
  /* src/index.scss */
2
4
  .squiz-fte-scope *,
3
5
  .squiz-fte-scope ::before,
@@ -674,6 +676,36 @@
674
676
  letter-spacing: -0.2px;
675
677
  line-height: 1.25rem;
676
678
  }
679
+ .squiz-fte-scope .code-block,
680
+ .squiz-fte-scope .preformatted {
681
+ display: flex;
682
+ }
683
+ .squiz-fte-scope .code-block .material-symbols-rounded,
684
+ .squiz-fte-scope .preformatted .material-symbols-rounded {
685
+ font-size: 1.125rem;
686
+ --tw-text-opacity: 1;
687
+ color: rgb(79 79 79 / var(--tw-text-opacity));
688
+ pointer-events: none;
689
+ margin-right: 0.375rem;
690
+ }
691
+ .squiz-fte-scope .code-block .block-divider,
692
+ .squiz-fte-scope .preformatted .block-divider {
693
+ --tw-bg-opacity: 1;
694
+ background-color: rgb(237 237 237 / var(--tw-bg-opacity));
695
+ width: 4px;
696
+ border-radius: 0.75rem;
697
+ margin-right: 0.625rem;
698
+ }
699
+ .squiz-fte-scope .code-block code,
700
+ .squiz-fte-scope .code-block pre,
701
+ .squiz-fte-scope .preformatted code,
702
+ .squiz-fte-scope .preformatted pre {
703
+ --tw-text-opacity: 1;
704
+ color: rgb(79 79 79 / var(--tw-text-opacity));
705
+ font-size: 0.75rem;
706
+ padding-top: 0.75rem;
707
+ padding-bottom: 0.75rem;
708
+ }
677
709
  .squiz-fte-scope .squiz-fte-form-group {
678
710
  display: flex;
679
711
  flex-direction: column;
@@ -959,6 +991,15 @@
959
991
  .squiz-fte-scope .dropdown-button:focus {
960
992
  background-color: rgba(0, 0, 0, 0.04);
961
993
  }
994
+ .squiz-fte-scope .dropdown-button svg {
995
+ font-size: 18px;
996
+ }
997
+ .squiz-fte-scope .dropdown-button .dropdown-button-label {
998
+ display: flex;
999
+ align-items: center;
1000
+ font-size: 14px;
1001
+ gap: 5px;
1002
+ }
962
1003
  .squiz-fte-scope .squiz-fte-checkbox {
963
1004
  --tw-text-opacity: 1;
964
1005
  color: rgb(61 61 61 / var(--tw-text-opacity));
@@ -4,6 +4,7 @@ type DropdownButtonProps = {
4
4
  isDisabled: boolean;
5
5
  isActive: boolean;
6
6
  label: string;
7
+ icon?: JSX.Element;
7
8
  };
8
- declare const DropdownButton: ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => JSX.Element;
9
+ declare const DropdownButton: ({ children, handleOnClick, isDisabled, isActive, label, icon }: DropdownButtonProps) => JSX.Element;
9
10
  export default DropdownButton;
@@ -4,10 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
- const Check_1 = __importDefault(require("@mui/icons-material/Check"));
8
- const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }) => {
7
+ const CheckRounded_1 = __importDefault(require("@mui/icons-material/CheckRounded"));
8
+ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label, icon }) => {
9
9
  return (react_1.default.createElement("button", { "aria-label": label, id: "dropdownMenuButton", title: label, type: "button", onClick: handleOnClick, disabled: isDisabled, className: `btn dropdown-button ${isActive ? 'is-active' : ''}` },
10
- react_1.default.createElement("span", null, children || label),
11
- isActive && react_1.default.createElement(Check_1.default, { className: "dropdown-button-icon" })));
10
+ react_1.default.createElement("div", { className: "dropdown-button-label" },
11
+ icon && icon,
12
+ children || label),
13
+ isActive && react_1.default.createElement(CheckRounded_1.default, { className: "dropdown-button-icon" })));
12
14
  };
13
15
  exports.default = DropdownButton;
@@ -4,9 +4,12 @@ exports.remirrorNodeToSquizNode = exports.resolveNodeTag = void 0;
4
4
  const undefinedIfEmpty_1 = require("../../undefinedIfEmpty");
5
5
  const Extensions_1 = require("../../../Extensions/Extensions");
6
6
  const resolveNodeTag = (node) => {
7
- if (node.type.name === 'text') {
7
+ if (node.type.name === Extensions_1.NodeName.Text) {
8
8
  return 'span';
9
9
  }
10
+ if (node.type.name === Extensions_1.NodeName.CodeBlock) {
11
+ return 'code';
12
+ }
10
13
  if (node.type.spec?.toDOM) {
11
14
  const domNode = node.type.spec.toDOM(node);
12
15
  if (domNode instanceof window.Node) {
@@ -63,9 +66,11 @@ const transformFragment = (fragment) => {
63
66
  return transformed;
64
67
  };
65
68
  const transformNode = (node) => {
66
- const attributes = node.type.name === Extensions_1.NodeName.Image ? transformAttributes(node.attrs) : undefined;
67
69
  const formattingOptions = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFormattingOptions(node));
68
70
  const font = (0, undefinedIfEmpty_1.undefinedIfEmpty)(resolveFontOptions(node));
71
+ const attributes = node.type.name === Extensions_1.NodeName.Image || node.type.name === Extensions_1.NodeName.CodeBlock
72
+ ? transformAttributes(node.attrs)
73
+ : undefined;
69
74
  let transformedNode = { type: 'text', value: node.text || '' };
70
75
  // Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
71
76
  // If the node has formatting options wrap in a tag.
@@ -21,6 +21,7 @@ const getNodeType = (node) => {
21
21
  p: 'paragraph',
22
22
  a: Extensions_1.NodeName.Text,
23
23
  span: Extensions_1.NodeName.Text,
24
+ code: Extensions_1.NodeName.CodeBlock,
24
25
  };
25
26
  if (typeMap[node.type]) {
26
27
  return typeMap[node.type];
@@ -43,6 +44,12 @@ const getNodeAttributes = (node) => {
43
44
  title: node.attributes?.title,
44
45
  };
45
46
  }
47
+ else if (node.type === 'tag' && node.tag === 'code') {
48
+ return {
49
+ language: node.attributes?.language || 'markup',
50
+ wrap: node.attributes?.wrap || true,
51
+ };
52
+ }
46
53
  else if (node.type === 'matrix-image') {
47
54
  return {
48
55
  matrixAssetId: node.matrixAssetId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.34.1-alpha.7",
3
+ "version": "1.34.1-alpha.8",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -20,8 +20,8 @@
20
20
  "@headlessui/react": "1.7.11",
21
21
  "@mui/icons-material": "5.11.16",
22
22
  "@remirror/react": "2.0.25",
23
- "@squiz/dx-json-schema-lib": "1.34.1-alpha.7",
24
- "@squiz/resource-browser": "1.34.1-alpha.7",
23
+ "@squiz/dx-json-schema-lib": "1.34.1-alpha.8",
24
+ "@squiz/resource-browser": "1.34.1-alpha.8",
25
25
  "clsx": "1.2.1",
26
26
  "react-hook-form": "7.43.2",
27
27
  "react-image-size": "2.0.0",
@@ -75,5 +75,5 @@
75
75
  "volta": {
76
76
  "node": "18.15.0"
77
77
  },
78
- "gitHead": "a96e5682ef09c76fb0eaadd1a0c44fb6140ee65e"
78
+ "gitHead": "6e1be2071c856fe3e6e4b14dbc1e67d93b97e12b"
79
79
  }
@@ -7,6 +7,7 @@ import Button from '../../../ui/Button/Button';
7
7
  import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
8
8
  import { NodeName } from '../../../Extensions/Extensions';
9
9
  import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
10
+ import { CodeBlockExtension } from 'remirror/dist-types/extensions';
10
11
 
11
12
  type ImageButtonProps = {
12
13
  inPopover?: boolean;
@@ -15,10 +16,10 @@ type ImageButtonProps = {
15
16
  const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
16
17
  const [showModal, setShowModal] = useState(false);
17
18
  const { insertImage, insertAssetImage } = useCommands<ImageExtension | AssetImageExtension>();
18
- const active = useActive<ImageExtension | AssetImageExtension>();
19
+ const active = useActive<ImageExtension | AssetImageExtension | CodeBlockExtension>();
19
20
  const selection = useCurrentSelection();
20
21
  // if the active selection is not an image, disable the button as it means it will be text
21
- const disabled = !selection.empty && !active.image() && !active.assetImage();
22
+ const disabled = (!selection.empty && !active.image() && !active.assetImage()) || active.codeBlock();
22
23
 
23
24
  const handleClick = () => {
24
25
  if (!showModal) {
@@ -9,6 +9,7 @@ import { CommandsExtension } from '../../../Extensions/CommandsExtension/Command
9
9
  import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
10
10
  import { MarkName } from '../../../Extensions/Extensions';
11
11
  import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
12
+ import { CodeBlockExtension } from 'remirror/dist-types/extensions';
12
13
 
13
14
  export type LinkButtonProps = {
14
15
  inPopover?: boolean;
@@ -17,9 +18,9 @@ export type LinkButtonProps = {
17
18
  const LinkButton = ({ inPopover = false }: LinkButtonProps) => {
18
19
  const [showModal, setShowModal] = useState(false);
19
20
  const { updateLink, updateAssetLink } = useCommands<AssetLinkExtension | LinkExtension | CommandsExtension>();
20
- const active = useActive<LinkExtension | AssetLinkExtension | ImageExtension>();
21
+ const active = useActive<LinkExtension | AssetLinkExtension | ImageExtension | CodeBlockExtension>();
21
22
  // If the image tool is active, disable the link tool as they shouldn't work at the same time
22
- const disabled = active.image();
23
+ const disabled = active.image() || active.codeBlock();
23
24
  const handleClick = () => {
24
25
  if (!showModal) {
25
26
  setShowModal(true);
@@ -0,0 +1,47 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, fireEvent } from '@testing-library/react';
3
+ import Editor from '../../../../Editor/Editor';
4
+ import React from 'react';
5
+
6
+ describe('Code block button', () => {
7
+ it('Renders the code block button', () => {
8
+ const { baseElement, getByRole } = render(<Editor />);
9
+ expect(baseElement).toBeTruthy();
10
+ expect(getByRole('button', { name: 'Code block' })).toBeTruthy();
11
+ });
12
+
13
+ it('Applies active status after selecting code block button', () => {
14
+ const { baseElement, getByRole } = render(<Editor />);
15
+ expect(baseElement).toBeTruthy();
16
+
17
+ const codeblockButton = getByRole('button', { name: 'Code block' });
18
+ expect(codeblockButton).toBeTruthy();
19
+ expect(codeblockButton.className).not.toContain('is-active');
20
+
21
+ fireEvent.click(codeblockButton);
22
+ expect(codeblockButton.className).toContain('is-active');
23
+ });
24
+
25
+ it('Should render a check icon if button is active', () => {
26
+ const { baseElement, getByRole } = render(<Editor />);
27
+ expect(baseElement).toBeTruthy();
28
+
29
+ const codeblockButton = getByRole('button', { name: 'Code block' });
30
+ expect(codeblockButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
31
+
32
+ fireEvent.click(codeblockButton);
33
+ expect(codeblockButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
34
+ });
35
+
36
+ it('Should apply preformatted tag and code tag to editor after clicking button', () => {
37
+ const { baseElement, getByRole } = render(<Editor />);
38
+ expect(baseElement).toBeTruthy();
39
+ expect(baseElement.querySelector('div.remirror-editor pre code')).toBeFalsy();
40
+
41
+ const codeblockButton = getByRole('button', { name: 'Code block' });
42
+ expect(codeblockButton).toBeTruthy();
43
+ fireEvent.click(codeblockButton);
44
+
45
+ expect(baseElement.querySelector(`div.remirror-editor pre code`)).toBeTruthy();
46
+ });
47
+ });
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive } from '@remirror/react';
3
+ import { ExtendedCodeBlockExtension } from '../../../../Extensions/CodeBlockExtension/CodeBlockExtension';
4
+ import DropdownButton from '../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton';
5
+ import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
6
+
7
+ const CodeBlockButton = () => {
8
+ const { toggleCodeBlock } = useCommands<ExtendedCodeBlockExtension>();
9
+
10
+ const active = useActive<ExtendedCodeBlockExtension>();
11
+ const enabled = toggleCodeBlock.enabled();
12
+
13
+ const handleSelect = () => {
14
+ if (toggleCodeBlock.enabled()) {
15
+ toggleCodeBlock();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <DropdownButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.codeBlock()}
24
+ label="Code block"
25
+ icon={<CodeRoundedIcon />}
26
+ >
27
+ <p>Code block</p>
28
+ </DropdownButton>
29
+ );
30
+ };
31
+
32
+ export default CodeBlockButton;
@@ -36,10 +36,10 @@ describe('Heading button', () => {
36
36
  expect(baseElement).toBeTruthy();
37
37
 
38
38
  const headingButton = getByRole('button', { name: label });
39
- expect(headingButton.querySelector('svg[data-testid="CheckIcon"]')).toBeFalsy();
39
+ expect(headingButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
40
40
 
41
41
  fireEvent.click(headingButton);
42
- expect(headingButton.querySelector('svg[data-testid="CheckIcon"]')).toBeTruthy();
42
+ expect(headingButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
43
43
  });
44
44
 
45
45
  it.each(headings)('Should apply "%s" heading tag to editor after clicking button', (label, tag) => {
@@ -25,6 +25,6 @@ describe('Paragraph button', () => {
25
25
 
26
26
  const paragraphButton = baseElement.querySelector('button[title="Paragraph"]') as HTMLButtonElement;
27
27
  expect(paragraphButton).toBeTruthy();
28
- expect(paragraphButton.querySelector('svg[data-testid="CheckIcon"]')).toBeTruthy();
28
+ expect(paragraphButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
29
29
  });
30
30
  });
@@ -27,10 +27,10 @@ describe('Preformatted button', () => {
27
27
  expect(baseElement).toBeTruthy();
28
28
 
29
29
  const preformattedButton = getByRole('button', { name: 'Preformatted' });
30
- expect(preformattedButton.querySelector('svg[data-testid="CheckIcon"]')).toBeFalsy();
30
+ expect(preformattedButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeFalsy();
31
31
 
32
32
  fireEvent.click(preformattedButton);
33
- expect(preformattedButton.querySelector('svg[data-testid="CheckIcon"]')).toBeTruthy();
33
+ expect(preformattedButton.querySelector('svg[data-testid="CheckRoundedIcon"]')).toBeTruthy();
34
34
  });
35
35
 
36
36
  it('Should apply preformatted tag to editor after clicking button', () => {
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { useCommands, useActive } from '@remirror/react';
3
3
  import { PreformattedExtension } from '../../../../Extensions/PreformattedExtension/PreformattedExtension';
4
4
  import DropdownButton from '../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton';
5
+ import ShortTextRoundedIcon from '@mui/icons-material/ShortTextRounded';
5
6
 
6
7
  const PreformattedButton = () => {
7
8
  const { togglePreformatted } = useCommands<PreformattedExtension>();
@@ -21,8 +22,9 @@ const PreformattedButton = () => {
21
22
  isDisabled={!enabled}
22
23
  isActive={active.preformatted()}
23
24
  label="Preformatted"
25
+ icon={<ShortTextRoundedIcon />}
24
26
  >
25
- <pre>Preformatted</pre>
27
+ <p>Preformatted</p>
26
28
  </DropdownButton>
27
29
  );
28
30
  };
@@ -2,24 +2,33 @@ import React from 'react';
2
2
  import HeadingButton from './Heading/HeadingButton';
3
3
  import ParagraphButton from './Paragraph/ParagraphButton';
4
4
  import PreformattedButton from './Preformatted/PreformattedButton';
5
+ import CodeBlockButton from './CodeBlock/CodeBlockButton';
5
6
  import ToolbarDropdown from '../../../ui/ToolbarDropdown/ToolbarDropdown';
6
7
  import { useActive, VerticalDivider } from '@remirror/react';
7
8
  import { PreformattedExtension } from '../../../Extensions/PreformattedExtension/PreformattedExtension';
9
+ import { CodeBlockExtension } from 'remirror/dist-types/extensions';
8
10
 
9
11
  const TextTypeDropdown = () => {
10
- const active = useActive<PreformattedExtension>();
12
+ const active = useActive<PreformattedExtension | CodeBlockExtension>();
11
13
 
12
14
  const activeLabel = () => {
13
15
  // Determine if preformatted is active
14
16
  if (active.preformatted()) {
15
17
  return 'Preformatted';
16
18
  }
19
+
20
+ // Determine if codeblock is active
21
+ if (active.codeBlock()) {
22
+ return 'Code block';
23
+ }
24
+
17
25
  // Determine if a heading is active
18
26
  for (let i = 1; i <= 6; i++) {
19
27
  if (active.heading({ level: i })) {
20
28
  return `Heading ${i}`;
21
29
  }
22
30
  }
31
+
23
32
  // Default to paragraph
24
33
  return 'Paragraph';
25
34
  };
@@ -35,6 +44,7 @@ const TextTypeDropdown = () => {
35
44
  <HeadingButton level={5} />
36
45
  <HeadingButton level={6} />
37
46
  <PreformattedButton />
47
+ <CodeBlockButton />
38
48
  </ToolbarDropdown>
39
49
  <VerticalDivider />
40
50
  </>
@@ -0,0 +1,34 @@
1
+ import { CodeBlockExtension } from '@remirror/extension-code-block';
2
+ import { NodeViewMethod, ProsemirrorNode } from 'remirror';
3
+
4
+ export class ExtendedCodeBlockExtension extends CodeBlockExtension {
5
+ createNodeViews(): NodeViewMethod {
6
+ return (node: ProsemirrorNode) => {
7
+ const { language } = node.attrs;
8
+
9
+ // This is the pre container for the code block
10
+ const dom = document.createElement('pre');
11
+ dom.setAttribute('spellcheck', 'false');
12
+ dom.classList.add(`code-block`);
13
+
14
+ // This is the actual code content in the code block
15
+ const contentDOM = document.createElement('code');
16
+ contentDOM.setAttribute('data-code-block-language', language);
17
+
18
+ // Divider between code block and pre container
19
+ const dividerElement = document.createElement('div');
20
+ dividerElement.classList.add('block-divider');
21
+
22
+ // The material icon to use
23
+ const codeIcon = document.createElement('svg');
24
+ codeIcon.classList.add('material-symbols-rounded');
25
+ codeIcon.textContent = 'code';
26
+
27
+ dom.append(codeIcon);
28
+ dom.append(dividerElement);
29
+ dom.append(contentDOM);
30
+
31
+ return { dom, contentDOM };
32
+ };
33
+ }
34
+ }
@@ -15,10 +15,12 @@ import { ImageExtension } from './ImageExtension/ImageExtension';
15
15
  import { CommandsExtension } from './CommandsExtension/CommandsExtension';
16
16
  import { EditorContextOptions } from '../Editor/EditorContext';
17
17
  import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
18
+ import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
18
19
  import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
19
20
 
20
21
  export enum NodeName {
21
22
  Image = 'image',
23
+ CodeBlock = 'codeBlock',
22
24
  AssetImage = 'assetImage',
23
25
  Text = 'text',
24
26
  }
@@ -38,6 +40,7 @@ export const createExtensions = (context: EditorContextOptions) => {
38
40
  new NodeFormattingExtension({ indents: [] }),
39
41
  new ParagraphExtension(),
40
42
  new PreformattedExtension(),
43
+ new ExtendedCodeBlockExtension({ defaultWrap: true }),
41
44
  new UnderlineExtension(),
42
45
  new HistoryExtension(),
43
46
  new ImageExtension(),
@@ -36,6 +36,8 @@ describe('PreformattedExtension', () => {
36
36
  },
37
37
  });
38
38
 
39
- expect(getHtmlContent()).toBe('<pre style="">This is some preformatted text</pre>');
39
+ expect(getHtmlContent()).toBe(
40
+ '<div class="preformatted"><svg class="material-symbols-rounded">short_text</svg><div class="block-divider"></div><pre data-node-text-align="null" style="text-align:null">This is some preformatted text</pre></div>',
41
+ );
40
42
  });
41
43
  });
@@ -10,6 +10,7 @@ import {
10
10
  ProsemirrorNode,
11
11
  toggleBlockItem,
12
12
  } from '@remirror/core';
13
+ import { NodeViewMethod } from 'remirror';
13
14
 
14
15
  @extension({})
15
16
  export class PreformattedExtension extends NodeExtension {
@@ -41,6 +42,36 @@ export class PreformattedExtension extends NodeExtension {
41
42
  };
42
43
  }
43
44
 
45
+ createNodeViews(): NodeViewMethod {
46
+ return (node: ProsemirrorNode) => {
47
+ const { nodeTextAlignment } = node.attrs;
48
+
49
+ // This is the pre container for the code block
50
+ const dom = document.createElement('div');
51
+ dom.classList.add(`preformatted`);
52
+
53
+ // This is the actual code content in the code block
54
+ const contentDOM = document.createElement('pre');
55
+ contentDOM.setAttribute('data-node-text-align', nodeTextAlignment);
56
+ contentDOM.setAttribute('style', `text-align:${nodeTextAlignment}`);
57
+
58
+ // Divider between code block and pre container
59
+ const dividerElement = document.createElement('div');
60
+ dividerElement.classList.add('block-divider');
61
+
62
+ // The material icon to use
63
+ const codeIcon = document.createElement('svg');
64
+ codeIcon.classList.add('material-symbols-rounded');
65
+ codeIcon.textContent = 'short_text';
66
+
67
+ dom.append(codeIcon);
68
+ dom.append(dividerElement);
69
+ dom.append(contentDOM);
70
+
71
+ return { dom, contentDOM };
72
+ };
73
+ }
74
+
44
75
  /**
45
76
  * Toggle the <pre> for the current block.
46
77
  */
package/src/index.scss CHANGED
@@ -3,6 +3,9 @@
3
3
  @import 'tailwindcss/components';
4
4
  @import 'tailwindcss/utilities';
5
5
 
6
+ /* So we can use icons inside of FTE content */
7
+ @import 'https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded';
8
+
6
9
  /* Global */
7
10
  @import './ui/typography';
8
11
  @import './ui/forms';
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import CheckIcon from '@mui/icons-material/Check';
2
+ import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
3
3
 
4
4
  type DropdownButtonProps = {
5
5
  children?: JSX.Element;
@@ -7,9 +7,10 @@ type DropdownButtonProps = {
7
7
  isDisabled: boolean;
8
8
  isActive: boolean;
9
9
  label: string;
10
+ icon?: JSX.Element;
10
11
  };
11
12
 
12
- const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }: DropdownButtonProps) => {
13
+ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label, icon }: DropdownButtonProps) => {
13
14
  return (
14
15
  <button
15
16
  aria-label={label}
@@ -20,8 +21,11 @@ const DropdownButton = ({ children, handleOnClick, isDisabled, isActive, label }
20
21
  disabled={isDisabled}
21
22
  className={`btn dropdown-button ${isActive ? 'is-active' : ''}`}
22
23
  >
23
- <span>{children || label}</span>
24
- {isActive && <CheckIcon className="dropdown-button-icon" />}
24
+ <div className="dropdown-button-label">
25
+ {icon && icon}
26
+ {children || label}
27
+ </div>
28
+ {isActive && <CheckRoundedIcon className="dropdown-button-icon" />}
25
29
  </button>
26
30
  );
27
31
  };
@@ -11,4 +11,15 @@
11
11
  &:focus {
12
12
  background-color: rgba(black, 0.04);
13
13
  }
14
+
15
+ svg {
16
+ font-size: 18px;
17
+ }
18
+
19
+ .dropdown-button-label {
20
+ display: flex;
21
+ align-items: center;
22
+ font-size: 14px;
23
+ gap: 5px;
24
+ }
14
25
  }
@@ -44,3 +44,29 @@ h6 {
44
44
  letter-spacing: -0.2px;
45
45
  line-height: 1.25rem;
46
46
  }
47
+
48
+ .code-block,
49
+ .preformatted {
50
+ display: flex;
51
+
52
+ .material-symbols-rounded {
53
+ @apply text-gray-700 text-xlg;
54
+ pointer-events: none;
55
+ margin-right: 0.375rem;
56
+ }
57
+
58
+ .block-divider {
59
+ @apply bg-gray-200;
60
+ width: 4px;
61
+ border-radius: 0.75rem;
62
+ margin-right: 0.625rem;
63
+ }
64
+
65
+ code,
66
+ pre {
67
+ @apply text-gray-700;
68
+ font-size: 0.75rem;
69
+ padding-top: 0.75rem;
70
+ padding-bottom: 0.75rem;
71
+ }
72
+ }
@@ -74,6 +74,43 @@ describe('remirrorNodeToSquizNode', () => {
74
74
  expect(result).toEqual(expected);
75
75
  });
76
76
 
77
+ it('should handle code block formatting', async () => {
78
+ const content: RemirrorJSON = {
79
+ type: 'doc',
80
+ content: [
81
+ {
82
+ type: 'codeBlock',
83
+ attrs: {
84
+ language: 'js',
85
+ wrap: true,
86
+ },
87
+ content: [
88
+ {
89
+ type: 'text',
90
+ text: 'Hello world',
91
+ },
92
+ ],
93
+ },
94
+ ],
95
+ };
96
+
97
+ const { editor } = await renderWithEditor(null, { content });
98
+
99
+ const expected: FormattedText = [
100
+ {
101
+ type: 'tag',
102
+ tag: 'code',
103
+ children: [{ type: 'text', value: 'Hello world' }],
104
+ attributes: {
105
+ language: 'js',
106
+ },
107
+ },
108
+ ];
109
+
110
+ const result = remirrorNodeToSquizNode(editor.doc);
111
+ expect(result).toEqual(expected);
112
+ });
113
+
77
114
  it('should handle images', async () => {
78
115
  const content: RemirrorJSON = {
79
116
  type: 'doc',
@@ -16,10 +16,14 @@ type FormattedNodeFontProperties = FormattedTextModels.v1.FormattedNodeFontPrope
16
16
  type FormattedNodeWithChildren = Extract<FormattedNode, { children: FormattedNode[] }>;
17
17
 
18
18
  export const resolveNodeTag = (node: ProsemirrorNode): string => {
19
- if (node.type.name === 'text') {
19
+ if (node.type.name === NodeName.Text) {
20
20
  return 'span';
21
21
  }
22
22
 
23
+ if (node.type.name === NodeName.CodeBlock) {
24
+ return 'code';
25
+ }
26
+
23
27
  if (node.type.spec?.toDOM) {
24
28
  const domNode = node.type.spec.toDOM(node);
25
29
 
@@ -92,9 +96,13 @@ const transformFragment = (fragment: Fragment): FormattedText => {
92
96
  };
93
97
 
94
98
  const transformNode = (node: ProsemirrorNode): FormattedNode => {
95
- const attributes = node.type.name === NodeName.Image ? transformAttributes(node.attrs) : undefined;
96
99
  const formattingOptions = undefinedIfEmpty(resolveFormattingOptions(node));
97
100
  const font = undefinedIfEmpty(resolveFontOptions(node));
101
+ const attributes =
102
+ node.type.name === NodeName.Image || node.type.name === NodeName.CodeBlock
103
+ ? transformAttributes(node.attrs)
104
+ : undefined;
105
+
98
106
  let transformedNode: FormattedNode = { type: 'text', value: node.text || '' };
99
107
 
100
108
  // Squiz "text" nodes can't have formatting/font options but Remirror "text" nodes can.
@@ -139,10 +139,10 @@ describe('squizNodeToRemirrorNode', () => {
139
139
  },
140
140
  ],
141
141
  type: 'tag',
142
- tag: 'code',
142
+ tag: 'video',
143
143
  },
144
144
  ],
145
- 'Unsupported node type provided: tag (tag: code)',
145
+ 'Unsupported node type provided: tag (tag: video)',
146
146
  ],
147
147
  [
148
148
  'Unsupported node type',
@@ -192,6 +192,43 @@ describe('squizNodeToRemirrorNode', () => {
192
192
  expect(result).toEqual(expected);
193
193
  });
194
194
 
195
+ it('should handle code block text', () => {
196
+ const squizComponentJSON: FormattedText = [
197
+ {
198
+ children: [
199
+ {
200
+ type: 'text',
201
+ value: 'Hello world!',
202
+ },
203
+ ],
204
+ type: 'tag',
205
+ tag: 'code',
206
+ },
207
+ ];
208
+
209
+ const expected: RemirrorJSON = {
210
+ content: [
211
+ {
212
+ type: 'codeBlock',
213
+ attrs: {
214
+ language: 'markup',
215
+ wrap: true,
216
+ },
217
+ content: [
218
+ {
219
+ type: 'text',
220
+ text: 'Hello world!',
221
+ },
222
+ ],
223
+ },
224
+ ],
225
+ type: 'doc',
226
+ };
227
+
228
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
229
+ expect(result).toEqual(expected);
230
+ });
231
+
195
232
  it('should handle images', () => {
196
233
  const squizComponentJSON: FormattedText = [
197
234
  {
@@ -26,6 +26,7 @@ const getNodeType = (node: FormattedNodes): string => {
26
26
  p: 'paragraph',
27
27
  a: NodeName.Text,
28
28
  span: NodeName.Text,
29
+ code: NodeName.CodeBlock,
29
30
  };
30
31
 
31
32
  if (typeMap[node.type]) {
@@ -53,6 +54,11 @@ const getNodeAttributes = (node: FormattedNodes): Attrs => {
53
54
  src: node.attributes?.src,
54
55
  title: node.attributes?.title,
55
56
  };
57
+ } else if (node.type === 'tag' && node.tag === 'code') {
58
+ return {
59
+ language: node.attributes?.language || 'markup',
60
+ wrap: node.attributes?.wrap || true,
61
+ };
56
62
  } else if (node.type === 'matrix-image') {
57
63
  return {
58
64
  matrixAssetId: node.matrixAssetId,
@@ -15,6 +15,6 @@ describe('getNodeNamesByGroup', () => {
15
15
  // Nodes in the first array will be transformed to a paragraph when formatting is cleared.
16
16
  // Nodes in the second array will be left as-is.
17
17
  expect(formattingNodeNames).toEqual(['paragraph', 'heading', 'preformatted']);
18
- expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'image']);
18
+ expect(otherNodeNames).toEqual(['assetImage', 'doc', 'text', 'codeBlock', 'image']);
19
19
  });
20
20
  });
@@ -27,9 +27,6 @@ module.exports = {
27
27
  'heading-3': ['1.125rem', '1.375rem'],
28
28
  'heading-4': ['1rem', '1.25rem'],
29
29
  },
30
- fontFamily: {
31
- base: 'Open Sans, Arial, sans-serif',
32
- },
33
30
  boxShadow: {
34
31
  outline: '0 0 0 1px rgba(0,0,0,0.10)',
35
32
  sm: '0 0 0 1px rgba(0,0,0,0.04), 0 1px 4px 2px rgba(0,0,0,0.08)',
@@ -75,6 +72,10 @@ module.exports = {
75
72
  300: '#0774d2',
76
73
  400: '#044985',
77
74
  },
75
+ teal: {
76
+ 100: '#E6F4F6',
77
+ 400: '#024752',
78
+ },
78
79
  red: {
79
80
  300: '#d72321',
80
81
  },