(null);
11 |
12 | useEffect(() => {
13 | switch (type) {
14 | case 'html':
15 | html(value).then(setCode);
16 | return;
17 | case 'json':
18 | json(value).then(setCode);
19 | return;
20 | }
21 | }, [setCode, value, type]);
22 |
23 | if (code === null) {
24 | return null;
25 | }
26 |
27 | return (
28 | {
32 | const s = window.getSelection();
33 | if (s === null) {
34 | return;
35 | }
36 | s.selectAllChildren(ev.currentTarget);
37 | }}
38 | />
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/helper/highlighters.tsx:
--------------------------------------------------------------------------------
1 | import hljs from 'highlight.js';
2 | import jsonHighlighter from 'highlight.js/lib/languages/json';
3 | import xmlHighlighter from 'highlight.js/lib/languages/xml';
4 | import prettierPluginBabel from 'prettier/plugins/babel';
5 | import prettierPluginEstree from 'prettier/plugins/estree';
6 | import prettierPluginHtml from 'prettier/plugins/html';
7 | import { format } from 'prettier/standalone';
8 |
9 | hljs.registerLanguage('json', jsonHighlighter);
10 | hljs.registerLanguage('html', xmlHighlighter);
11 |
12 | export async function html(value: string): Promise {
13 | const prettyValue = await format(value, {
14 | parser: 'html',
15 | plugins: [prettierPluginHtml],
16 | });
17 | return hljs.highlight(prettyValue, { language: 'html' }).value;
18 | }
19 |
20 | export async function json(value: string): Promise {
21 | const prettyValue = await format(value, {
22 | parser: 'json',
23 | printWidth: 0,
24 | trailingComma: 'all',
25 | plugins: [prettierPluginBabel, prettierPluginEstree],
26 | });
27 | return hljs.highlight(prettyValue, { language: 'javascript' }).value;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Stack, useTheme } from '@mui/material';
4 |
5 | import { useInspectorDrawerOpen, useSamplesDrawerOpen } from '../documents/editor/EditorContext';
6 |
7 | import InspectorDrawer, { INSPECTOR_DRAWER_WIDTH } from './InspectorDrawer';
8 | import SamplesDrawer, { SAMPLES_DRAWER_WIDTH } from './SamplesDrawer';
9 | import TemplatePanel from './TemplatePanel';
10 |
11 | function useDrawerTransition(cssProperty: 'margin-left' | 'margin-right', open: boolean) {
12 | const { transitions } = useTheme();
13 | return transitions.create(cssProperty, {
14 | easing: !open ? transitions.easing.sharp : transitions.easing.easeOut,
15 | duration: !open ? transitions.duration.leavingScreen : transitions.duration.enteringScreen,
16 | });
17 | }
18 |
19 | export default function App() {
20 | const inspectorDrawerOpen = useInspectorDrawerOpen();
21 | const samplesDrawerOpen = useSamplesDrawerOpen();
22 |
23 | const marginLeftTransition = useDrawerTransition('margin-left', samplesDrawerOpen);
24 | const marginRightTransition = useDrawerTransition('margin-right', inspectorDrawerOpen);
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container';
4 |
5 | import { useCurrentBlockId } from '../../editor/EditorBlock';
6 | import { setDocument, setSelectedBlockId } from '../../editor/EditorContext';
7 | import EditorChildrenIds, { EditorChildrenChange } from '../helpers/EditorChildrenIds';
8 |
9 | import ColumnsContainerPropsSchema, { ColumnsContainerProps } from './ColumnsContainerPropsSchema';
10 |
11 | const EMPTY_COLUMNS = [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }];
12 |
13 | export default function ColumnsContainerEditor({ style, props }: ColumnsContainerProps) {
14 | const currentBlockId = useCurrentBlockId();
15 |
16 | const { columns, ...restProps } = props ?? {};
17 | const columnsValue = columns ?? EMPTY_COLUMNS;
18 |
19 | const updateColumn = (columnIndex: 0 | 1 | 2, { block, blockId, childrenIds }: EditorChildrenChange) => {
20 | const nColumns = [...columnsValue];
21 | nColumns[columnIndex] = { childrenIds };
22 | setDocument({
23 | [blockId]: block,
24 | [currentBlockId]: {
25 | type: 'ColumnsContainer',
26 | data: ColumnsContainerPropsSchema.parse({
27 | style,
28 | props: {
29 | ...restProps,
30 | columns: nColumns,
31 | },
32 | }),
33 | },
34 | });
35 | setSelectedBlockId(blockId);
36 | };
37 |
38 | return (
39 | updateColumn(0, change)} />,
44 | updateColumn(1, change)} />,
45 | updateColumn(2, change)} />,
46 | ]}
47 | />
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container';
4 |
5 | const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape;
6 |
7 | const ColumnsContainerPropsSchema = z.object({
8 | style: BaseColumnsContainerPropsSchema.shape.style,
9 | props: z
10 | .object({
11 | ...BasePropsShape,
12 | columns: z.tuple([
13 | z.object({ childrenIds: z.array(z.string()) }),
14 | z.object({ childrenIds: z.array(z.string()) }),
15 | z.object({ childrenIds: z.array(z.string()) }),
16 | ]),
17 | })
18 | .optional()
19 | .nullable(),
20 | });
21 |
22 | export type ColumnsContainerProps = z.infer;
23 | export default ColumnsContainerPropsSchema;
24 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container as BaseContainer } from '@usewaypoint/block-container';
4 |
5 | import { useCurrentBlockId } from '../../editor/EditorBlock';
6 | import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext';
7 | import EditorChildrenIds from '../helpers/EditorChildrenIds';
8 |
9 | import { ContainerProps } from './ContainerPropsSchema';
10 |
11 | export default function ContainerEditor({ style, props }: ContainerProps) {
12 | const childrenIds = props?.childrenIds ?? [];
13 |
14 | const document = useDocument();
15 | const currentBlockId = useCurrentBlockId();
16 |
17 | return (
18 |
19 | {
22 | setDocument({
23 | [blockId]: block,
24 | [currentBlockId]: {
25 | type: 'Container',
26 | data: {
27 | ...document[currentBlockId].data,
28 | props: { childrenIds: childrenIds },
29 | },
30 | },
31 | });
32 | setSelectedBlockId(blockId);
33 | }}
34 | />
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container';
4 |
5 | const ContainerPropsSchema = z.object({
6 | style: BaseContainerPropsSchema.shape.style,
7 | props: z
8 | .object({
9 | childrenIds: z.array(z.string()).optional().nullable(),
10 | })
11 | .optional()
12 | .nullable(),
13 | });
14 |
15 | export default ContainerPropsSchema;
16 |
17 | export type ContainerProps = z.infer;
18 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const COLOR_SCHEMA = z
4 | .string()
5 | .regex(/^#[0-9a-fA-F]{6}$/)
6 | .nullable()
7 | .optional();
8 |
9 | const FONT_FAMILY_SCHEMA = z
10 | .enum([
11 | 'MODERN_SANS',
12 | 'BOOK_SANS',
13 | 'ORGANIC_SANS',
14 | 'GEOMETRIC_SANS',
15 | 'HEAVY_SANS',
16 | 'ROUNDED_SANS',
17 | 'MODERN_SERIF',
18 | 'BOOK_SERIF',
19 | 'MONOSPACE',
20 | ])
21 | .nullable()
22 | .optional();
23 |
24 | const EmailLayoutPropsSchema = z.object({
25 | backdropColor: COLOR_SCHEMA,
26 | borderColor: COLOR_SCHEMA,
27 | borderRadius: z.number().optional().nullable(),
28 | canvasColor: COLOR_SCHEMA,
29 | textColor: COLOR_SCHEMA,
30 | fontFamily: FONT_FAMILY_SCHEMA,
31 | childrenIds: z.array(z.string()).optional().nullable(),
32 | });
33 |
34 | export default EmailLayoutPropsSchema;
35 |
36 | export type EmailLayoutProps = z.infer;
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/BlockButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Button, SxProps, Typography } from '@mui/material';
4 |
5 | type BlockMenuButtonProps = {
6 | label: string;
7 | icon: React.ReactNode;
8 | onClick: () => void;
9 | };
10 |
11 | const BUTTON_SX: SxProps = { p: 1.5, display: 'flex', flexDirection: 'column' };
12 | const ICON_SX: SxProps = {
13 | mb: 0.75,
14 | width: '100%',
15 | bgcolor: 'cadet.200',
16 | display: 'flex',
17 | justifyContent: 'center',
18 | p: 1,
19 | border: '1px solid',
20 | borderColor: 'cadet.300',
21 | };
22 |
23 | export default function BlockTypeButton({ label, icon, onClick }: BlockMenuButtonProps) {
24 | return (
25 | {
28 | ev.stopPropagation();
29 | onClick();
30 | }}
31 | >
32 | {icon}
33 | {label}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/BlocksMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Menu } from '@mui/material';
4 |
5 | import { TEditorBlock } from '../../../../editor/core';
6 |
7 | import BlockButton from './BlockButton';
8 | import { BUTTONS } from './buttons';
9 |
10 | type BlocksMenuProps = {
11 | anchorEl: HTMLElement | null;
12 | setAnchorEl: (v: HTMLElement | null) => void;
13 | onSelect: (block: TEditorBlock) => void;
14 | };
15 | export default function BlocksMenu({ anchorEl, setAnchorEl, onSelect }: BlocksMenuProps) {
16 | const onClose = () => {
17 | setAnchorEl(null);
18 | };
19 |
20 | const onClick = (block: TEditorBlock) => {
21 | onSelect(block);
22 | setAnchorEl(null);
23 | };
24 |
25 | if (anchorEl === null) {
26 | return null;
27 | }
28 |
29 | return (
30 |
37 |
38 | {BUTTONS.map((k, i) => (
39 | onClick(k.block())} />
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/DividerButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import { AddOutlined } from '@mui/icons-material';
4 | import { Fade, IconButton } from '@mui/material';
5 |
6 | type Props = {
7 | buttonElement: HTMLElement | null;
8 | onClick: () => void;
9 | };
10 | export default function DividerButton({ buttonElement, onClick }: Props) {
11 | const [visible, setVisible] = useState(false);
12 |
13 | useEffect(() => {
14 | function listener({ clientX, clientY }: MouseEvent) {
15 | if (!buttonElement) {
16 | return;
17 | }
18 | const rect = buttonElement.getBoundingClientRect();
19 | const rectY = rect.y;
20 | const bottomX = rect.x;
21 | const topX = bottomX + rect.width;
22 |
23 | if (Math.abs(clientY - rectY) < 20) {
24 | if (bottomX < clientX && clientX < topX) {
25 | setVisible(true);
26 | return;
27 | }
28 | }
29 | setVisible(false);
30 | }
31 | window.addEventListener('mousemove', listener);
32 | return () => {
33 | window.removeEventListener('mousemove', listener);
34 | };
35 | }, [buttonElement, setVisible]);
36 |
37 | return (
38 |
39 | {
56 | ev.stopPropagation();
57 | onClick();
58 | }}
59 | >
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/PlaceholderButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AddOutlined } from '@mui/icons-material';
4 | import { ButtonBase } from '@mui/material';
5 |
6 | type Props = {
7 | onClick: () => void;
8 | };
9 | export default function PlaceholderButton({ onClick }: Props) {
10 | return (
11 | {
13 | ev.stopPropagation();
14 | onClick();
15 | }}
16 | sx={{
17 | display: 'flex',
18 | alignContent: 'center',
19 | justifyContent: 'center',
20 | height: 48,
21 | width: '100%',
22 | bgcolor: 'rgba(0,0,0, 0.05)',
23 | }}
24 | >
25 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { TEditorBlock } from '../../../../editor/core';
4 |
5 | import BlocksMenu from './BlocksMenu';
6 | import DividerButton from './DividerButton';
7 | import PlaceholderButton from './PlaceholderButton';
8 |
9 | type Props = {
10 | placeholder?: boolean;
11 | onSelect: (block: TEditorBlock) => void;
12 | };
13 | export default function AddBlockButton({ onSelect, placeholder }: Props) {
14 | const [menuAnchorEl, setMenuAnchorEl] = useState(null);
15 | const [buttonElement, setButtonElement] = useState(null);
16 |
17 | const handleButtonClick = () => {
18 | setMenuAnchorEl(buttonElement);
19 | };
20 |
21 | const renderButton = () => {
22 | if (placeholder) {
23 | return ;
24 | } else {
25 | return ;
26 | }
27 | };
28 |
29 | return (
30 | <>
31 |
32 | {renderButton()}
33 |
34 |
35 | >
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import { TEditorBlock } from '../../../editor/core';
4 | import EditorBlock from '../../../editor/EditorBlock';
5 |
6 | import AddBlockButton from './AddBlockMenu';
7 |
8 | export type EditorChildrenChange = {
9 | blockId: string;
10 | block: TEditorBlock;
11 | childrenIds: string[];
12 | };
13 |
14 | function generateId() {
15 | return `block-${Date.now()}`;
16 | }
17 |
18 | export type EditorChildrenIdsProps = {
19 | childrenIds: string[] | null | undefined;
20 | onChange: (val: EditorChildrenChange) => void;
21 | };
22 | export default function EditorChildrenIds({ childrenIds, onChange }: EditorChildrenIdsProps) {
23 | const appendBlock = (block: TEditorBlock) => {
24 | const blockId = generateId();
25 | return onChange({
26 | blockId,
27 | block,
28 | childrenIds: [...(childrenIds || []), blockId],
29 | });
30 | };
31 |
32 | const insertBlock = (block: TEditorBlock, index: number) => {
33 | const blockId = generateId();
34 | const newChildrenIds = [...(childrenIds || [])];
35 | newChildrenIds.splice(index, 0, blockId);
36 | return onChange({
37 | blockId,
38 | block,
39 | childrenIds: newChildrenIds,
40 | });
41 | };
42 |
43 | if (!childrenIds || childrenIds.length === 0) {
44 | return ;
45 | }
46 |
47 | return (
48 | <>
49 | {childrenIds.map((childId, i) => (
50 |
51 | insertBlock(block, i)} />
52 |
53 |
54 | ))}
55 |
56 | >
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/TStyle.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | export type TStyle = {
4 | backgroundColor?: any;
5 | borderColor?: any;
6 | borderRadius?: any;
7 | color?: any;
8 | fontFamily?: any;
9 | fontSize?: any;
10 | fontWeight?: any;
11 | padding?: any;
12 | textAlign?: any;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/EditorBlockWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useState } from 'react';
2 |
3 | import { Box } from '@mui/material';
4 |
5 | import { useCurrentBlockId } from '../../../editor/EditorBlock';
6 | import { setSelectedBlockId, useSelectedBlockId } from '../../../editor/EditorContext';
7 |
8 | import TuneMenu from './TuneMenu';
9 |
10 | type TEditorBlockWrapperProps = {
11 | children: JSX.Element;
12 | };
13 |
14 | export default function EditorBlockWrapper({ children }: TEditorBlockWrapperProps) {
15 | const selectedBlockId = useSelectedBlockId();
16 | const [mouseInside, setMouseInside] = useState(false);
17 | const blockId = useCurrentBlockId();
18 |
19 | let outline: CSSProperties['outline'];
20 | if (selectedBlockId === blockId) {
21 | outline = '2px solid rgba(0,121,204, 1)';
22 | } else if (mouseInside) {
23 | outline = '2px solid rgba(0,121,204, 0.3)';
24 | }
25 |
26 | const renderMenu = () => {
27 | if (selectedBlockId !== blockId) {
28 | return null;
29 | }
30 | return ;
31 | };
32 |
33 | return (
34 | {
42 | setMouseInside(true);
43 | ev.stopPropagation();
44 | }}
45 | onMouseLeave={() => {
46 | setMouseInside(false);
47 | }}
48 | onClick={(ev) => {
49 | setSelectedBlockId(blockId);
50 | ev.stopPropagation();
51 | ev.preventDefault();
52 | }}
53 | >
54 | {renderMenu()}
55 | {children}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/ReaderBlockWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 |
3 | import { TStyle } from '../TStyle';
4 |
5 | type TReaderBlockWrapperProps = {
6 | style: TStyle;
7 | children: JSX.Element;
8 | };
9 |
10 | export default function ReaderBlockWrapper({ style, children }: TReaderBlockWrapperProps) {
11 | const { padding, borderColor, ...restStyle } = style;
12 | const cssStyle: CSSProperties = {
13 | ...restStyle,
14 | };
15 |
16 | if (padding) {
17 | const { top, bottom, left, right } = padding;
18 | cssStyle.padding = `${top}px ${right}px ${bottom}px ${left}px`;
19 | }
20 |
21 | if (borderColor) {
22 | cssStyle.border = `1px solid ${borderColor}`;
23 | }
24 |
25 | return {children}
;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/fontFamily.ts:
--------------------------------------------------------------------------------
1 | export const FONT_FAMILIES = [
2 | {
3 | key: 'MODERN_SANS',
4 | label: 'Modern sans',
5 | value: '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif',
6 | },
7 | {
8 | key: 'BOOK_SANS',
9 | label: 'Book sans',
10 | value: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif',
11 | },
12 | {
13 | key: 'ORGANIC_SANS',
14 | label: 'Organic sans',
15 | value: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif',
16 | },
17 | {
18 | key: 'GEOMETRIC_SANS',
19 | label: 'Geometric sans',
20 | value: 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif',
21 | },
22 | {
23 | key: 'HEAVY_SANS',
24 | label: 'Heavy sans',
25 | value:
26 | 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif',
27 | },
28 | {
29 | key: 'ROUNDED_SANS',
30 | label: 'Rounded sans',
31 | value:
32 | 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif',
33 | },
34 | {
35 | key: 'MODERN_SERIF',
36 | label: 'Modern serif',
37 | value: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif',
38 | },
39 | {
40 | key: 'BOOK_SERIF',
41 | label: 'Book serif',
42 | value: '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif',
43 | },
44 | {
45 | key: 'MONOSPACE',
46 | label: 'Monospace',
47 | value: '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace',
48 | },
49 | ];
50 |
51 | export const FONT_FAMILY_NAMES = [
52 | 'MODERN_SANS',
53 | 'BOOK_SANS',
54 | 'ORGANIC_SANS',
55 | 'GEOMETRIC_SANS',
56 | 'HEAVY_SANS',
57 | 'ROUNDED_SANS',
58 | 'MODERN_SERIF',
59 | 'BOOK_SERIF',
60 | 'MONOSPACE',
61 | ] as const;
62 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/zod.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { FONT_FAMILY_NAMES } from './fontFamily';
4 |
5 | export function zColor() {
6 | return z.string().regex(/^#[0-9a-fA-F]{6}$/);
7 | }
8 |
9 | export function zFontFamily() {
10 | return z.enum(FONT_FAMILY_NAMES);
11 | }
12 |
13 | export function zFontWeight() {
14 | return z.enum(['bold', 'normal']);
15 | }
16 |
17 | export function zTextAlign() {
18 | return z.enum(['left', 'center', 'right']);
19 | }
20 |
21 | export function zPadding() {
22 | return z.object({
23 | top: z.number(),
24 | bottom: z.number(),
25 | right: z.number(),
26 | left: z.number(),
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/editor/EditorBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 |
3 | import { EditorBlock as CoreEditorBlock } from './core';
4 | import { useDocument } from './EditorContext';
5 |
6 | const EditorBlockContext = createContext(null);
7 | export const useCurrentBlockId = () => useContext(EditorBlockContext)!;
8 |
9 | type EditorBlockProps = {
10 | id: string;
11 | };
12 |
13 | /**
14 | *
15 | * @param id - Block id
16 | * @returns EditorBlock component that loads data from the EditorDocumentContext
17 | */
18 | export default function EditorBlock({ id }: EditorBlockProps) {
19 | const document = useDocument();
20 | const block = document[id];
21 | if (!block) {
22 | throw new Error('Could not find block');
23 | }
24 | return (
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/editor/EditorContext.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import getConfiguration from '../../getConfiguration';
4 |
5 | import { TEditorConfiguration } from './core';
6 |
7 | type TValue = {
8 | document: TEditorConfiguration;
9 |
10 | selectedBlockId: string | null;
11 | selectedSidebarTab: 'block-configuration' | 'styles';
12 | selectedMainTab: 'editor' | 'preview' | 'json' | 'html';
13 | selectedScreenSize: 'desktop' | 'mobile';
14 |
15 | inspectorDrawerOpen: boolean;
16 | samplesDrawerOpen: boolean;
17 | };
18 |
19 | const editorStateStore = create(() => ({
20 | document: getConfiguration(window.location.hash),
21 | selectedBlockId: null,
22 | selectedSidebarTab: 'styles',
23 | selectedMainTab: 'editor',
24 | selectedScreenSize: 'desktop',
25 |
26 | inspectorDrawerOpen: true,
27 | samplesDrawerOpen: true,
28 | }));
29 |
30 | export function useDocument() {
31 | return editorStateStore((s) => s.document);
32 | }
33 |
34 | export function useSelectedBlockId() {
35 | return editorStateStore((s) => s.selectedBlockId);
36 | }
37 |
38 | export function useSelectedScreenSize() {
39 | return editorStateStore((s) => s.selectedScreenSize);
40 | }
41 |
42 | export function useSelectedMainTab() {
43 | return editorStateStore((s) => s.selectedMainTab);
44 | }
45 |
46 | export function setSelectedMainTab(selectedMainTab: TValue['selectedMainTab']) {
47 | return editorStateStore.setState({ selectedMainTab });
48 | }
49 |
50 | export function useSelectedSidebarTab() {
51 | return editorStateStore((s) => s.selectedSidebarTab);
52 | }
53 |
54 | export function useInspectorDrawerOpen() {
55 | return editorStateStore((s) => s.inspectorDrawerOpen);
56 | }
57 |
58 | export function useSamplesDrawerOpen() {
59 | return editorStateStore((s) => s.samplesDrawerOpen);
60 | }
61 |
62 | export function setSelectedBlockId(selectedBlockId: TValue['selectedBlockId']) {
63 | const selectedSidebarTab = selectedBlockId === null ? 'styles' : 'block-configuration';
64 | const options: Partial = {};
65 | if (selectedBlockId !== null) {
66 | options.inspectorDrawerOpen = true;
67 | }
68 | return editorStateStore.setState({
69 | selectedBlockId,
70 | selectedSidebarTab,
71 | ...options,
72 | });
73 | }
74 |
75 | export function setSidebarTab(selectedSidebarTab: TValue['selectedSidebarTab']) {
76 | return editorStateStore.setState({ selectedSidebarTab });
77 | }
78 |
79 | export function resetDocument(document: TValue['document']) {
80 | return editorStateStore.setState({
81 | document,
82 | selectedSidebarTab: 'styles',
83 | selectedBlockId: null,
84 | });
85 | }
86 |
87 | export function setDocument(document: TValue['document']) {
88 | const originalDocument = editorStateStore.getState().document;
89 | return editorStateStore.setState({
90 | document: {
91 | ...originalDocument,
92 | ...document,
93 | },
94 | });
95 | }
96 |
97 | export function toggleInspectorDrawerOpen() {
98 | const inspectorDrawerOpen = !editorStateStore.getState().inspectorDrawerOpen;
99 | return editorStateStore.setState({ inspectorDrawerOpen });
100 | }
101 |
102 | export function toggleSamplesDrawerOpen() {
103 | const samplesDrawerOpen = !editorStateStore.getState().samplesDrawerOpen;
104 | return editorStateStore.setState({ samplesDrawerOpen });
105 | }
106 |
107 | export function setSelectedScreenSize(selectedScreenSize: TValue['selectedScreenSize']) {
108 | return editorStateStore.setState({ selectedScreenSize });
109 | }
110 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon.ico
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/index.tsx:
--------------------------------------------------------------------------------
1 | import EMPTY_EMAIL_MESSAGE from './sample/empty-email-message';
2 | import ONE_TIME_PASSCODE from './sample/one-time-passcode';
3 | import ORDER_ECOMMERCE from './sample/order-ecommerce';
4 | import POST_METRICS_REPORT from './sample/post-metrics-report';
5 | import RESERVATION_REMINDER from './sample/reservation-reminder';
6 | import RESET_PASSWORD from './sample/reset-password';
7 | import RESPOND_TO_MESSAGE from './sample/respond-to-message';
8 | import SUBSCRIPTION_RECEIPT from './sample/subscription-receipt';
9 | import WELCOME from './sample/welcome';
10 |
11 | export default function getConfiguration(template: string) {
12 | if (template.startsWith('#sample/')) {
13 | const sampleName = template.replace('#sample/', '');
14 | switch (sampleName) {
15 | case 'welcome':
16 | return WELCOME;
17 | case 'one-time-password':
18 | return ONE_TIME_PASSCODE;
19 | case 'order-ecomerce':
20 | return ORDER_ECOMMERCE;
21 | case 'post-metrics-report':
22 | return POST_METRICS_REPORT;
23 | case 'reservation-reminder':
24 | return RESERVATION_REMINDER;
25 | case 'reset-password':
26 | return RESET_PASSWORD;
27 | case 'respond-to-message':
28 | return RESPOND_TO_MESSAGE;
29 | case 'subscription-receipt':
30 | return SUBSCRIPTION_RECEIPT;
31 | }
32 | }
33 |
34 | if (template.startsWith('#code/')) {
35 | const encodedString = template.replace('#code/', '');
36 | const configurationString = decodeURIComponent(atob(encodedString));
37 | try {
38 | return JSON.parse(configurationString);
39 | } catch {
40 | console.error(`Couldn't load configuration from hash.`);
41 | }
42 | }
43 |
44 | return EMPTY_EMAIL_MESSAGE;
45 | }
46 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/sample/empty-email-message.ts:
--------------------------------------------------------------------------------
1 | import { TEditorConfiguration } from '../../documents/editor/core';
2 |
3 | const EMPTY_EMAIL_MESSAGE: TEditorConfiguration = {
4 | root: {
5 | type: 'EmailLayout',
6 | data: {
7 | backdropColor: '#F5F5F5',
8 | canvasColor: '#FFFFFF',
9 | textColor: '#262626',
10 | fontFamily: 'MODERN_SANS',
11 | childrenIds: [],
12 | },
13 | },
14 | };
15 |
16 | export default EMPTY_EMAIL_MESSAGE;
17 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/sample/one-time-passcode.ts:
--------------------------------------------------------------------------------
1 | import { TEditorConfiguration } from '../../documents/editor/core';
2 |
3 | const ONE_TIME_PASSCODE: TEditorConfiguration = {
4 | root: {
5 | type: 'EmailLayout',
6 | data: {
7 | backdropColor: '#000000',
8 | canvasColor: '#000000',
9 | textColor: '#FFFFFF',
10 | fontFamily: 'BOOK_SERIF',
11 | childrenIds: [
12 | 'block_ChPX66qUhF46uynDE8AY11',
13 | 'block_CkNrtQgkqPt2YWLv1hr5eJ',
14 | 'block_BFLBa3q5y8kax9KngyXP65',
15 | 'block_4T7sDFb4rqbSyWjLGJKmov',
16 | 'block_Rvc8ZfTjfhXjpphHquJKvP',
17 | ],
18 | },
19 | },
20 | block_ChPX66qUhF46uynDE8AY11: {
21 | type: 'Image',
22 | data: {
23 | style: {
24 | backgroundColor: null,
25 | padding: {
26 | top: 24,
27 | bottom: 24,
28 | left: 24,
29 | right: 24,
30 | },
31 | textAlign: 'center',
32 | },
33 | props: {
34 | height: 24,
35 | url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_jc7ZfPvdHJ6rtH1W/&.png',
36 | contentAlignment: 'middle',
37 | },
38 | },
39 | },
40 | block_CkNrtQgkqPt2YWLv1hr5eJ: {
41 | type: 'Text',
42 | data: {
43 | style: {
44 | color: '#ffffff',
45 | backgroundColor: null,
46 | fontSize: 16,
47 | fontFamily: null,
48 | fontWeight: 'normal',
49 | textAlign: 'center',
50 | padding: {
51 | top: 16,
52 | bottom: 16,
53 | left: 24,
54 | right: 24,
55 | },
56 | },
57 | props: {
58 | text: 'Here is your one-time passcode:',
59 | },
60 | },
61 | },
62 | block_BFLBa3q5y8kax9KngyXP65: {
63 | type: 'Heading',
64 | data: {
65 | style: {
66 | color: null,
67 | backgroundColor: null,
68 | fontFamily: 'MONOSPACE',
69 | fontWeight: 'bold',
70 | textAlign: 'center',
71 | padding: {
72 | top: 16,
73 | bottom: 16,
74 | left: 24,
75 | right: 24,
76 | },
77 | },
78 | props: {
79 | level: 'h1',
80 | text: '0123456',
81 | },
82 | },
83 | },
84 | block_4T7sDFb4rqbSyWjLGJKmov: {
85 | type: 'Text',
86 | data: {
87 | style: {
88 | color: '#868686',
89 | backgroundColor: null,
90 | fontSize: 16,
91 | fontFamily: null,
92 | fontWeight: 'normal',
93 | textAlign: 'center',
94 | padding: {
95 | top: 16,
96 | bottom: 16,
97 | left: 24,
98 | right: 24,
99 | },
100 | },
101 | props: {
102 | text: 'This code will expire in 30 minutes.',
103 | },
104 | },
105 | },
106 | block_Rvc8ZfTjfhXjpphHquJKvP: {
107 | type: 'Text',
108 | data: {
109 | style: {
110 | color: '#868686',
111 | backgroundColor: null,
112 | fontSize: 14,
113 | fontFamily: null,
114 | fontWeight: 'normal',
115 | textAlign: 'center',
116 | padding: {
117 | top: 16,
118 | bottom: 16,
119 | left: 24,
120 | right: 24,
121 | },
122 | },
123 | props: {
124 | text: 'Problems? Just reply to this email.',
125 | },
126 | },
127 | },
128 | };
129 |
130 | export default ONE_TIME_PASSCODE;
131 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import { CssBaseline, ThemeProvider } from '@mui/material';
5 |
6 | import App from './App';
7 | import theme from './theme';
8 |
9 | ReactDOM.createRoot(document.getElementById('root')!).render(
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/editor-sample/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor-sample/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | import react from '@vitejs/plugin-react-swc';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: '/email-builder-js/',
8 | });
9 |
--------------------------------------------------------------------------------
/packages/email-builder/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tests
9 | tsconfig.json
--------------------------------------------------------------------------------
/packages/email-builder/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
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 |
--------------------------------------------------------------------------------
/packages/email-builder/README.md:
--------------------------------------------------------------------------------
1 | # usewaypoint/email-builder
2 |
--------------------------------------------------------------------------------
/packages/email-builder/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/email-builder",
3 | "version": "0.0.8",
4 | "description": "React component to render email messages",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "build": "npx tsc"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "react-dom": "^16 || ^17 || ^18",
26 | "zod": "^1 || ^2 || ^3"
27 | },
28 | "dependencies": {
29 | "@usewaypoint/block-avatar": "^0.0.3",
30 | "@usewaypoint/block-button": "^0.0.3",
31 | "@usewaypoint/block-columns-container": "^0.0.3",
32 | "@usewaypoint/block-container": "^0.0.2",
33 | "@usewaypoint/block-divider": "^0.0.4",
34 | "@usewaypoint/block-heading": "^0.0.3",
35 | "@usewaypoint/block-html": "^0.0.3",
36 | "@usewaypoint/block-image": "^0.0.5",
37 | "@usewaypoint/block-spacer": "^0.0.3",
38 | "@usewaypoint/block-text": "^0.0.6",
39 | "@usewaypoint/document-core": "^0.0.6"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container';
4 |
5 | const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape;
6 |
7 | const ColumnsContainerPropsSchema = z.object({
8 | style: BaseColumnsContainerPropsSchema.shape.style,
9 | props: z
10 | .object({
11 | ...BasePropsShape,
12 | columns: z.tuple([
13 | z.object({ childrenIds: z.array(z.string()) }),
14 | z.object({ childrenIds: z.array(z.string()) }),
15 | z.object({ childrenIds: z.array(z.string()) }),
16 | ]),
17 | })
18 | .optional()
19 | .nullable(),
20 | });
21 |
22 | export default ColumnsContainerPropsSchema;
23 | export type ColumnsContainerProps = z.infer;
24 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container';
4 |
5 | import { ReaderBlock } from '../../Reader/core';
6 |
7 | import { ColumnsContainerProps } from './ColumnsContainerPropsSchema';
8 |
9 | export default function ColumnsContainerReader({ style, props }: ColumnsContainerProps) {
10 | const { columns, ...restProps } = props ?? {};
11 | let cols = undefined;
12 | if (columns) {
13 | cols = columns.map((col) => col.childrenIds.map((childId) => ));
14 | }
15 |
16 | return ;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container';
4 |
5 | export const ContainerPropsSchema = z.object({
6 | style: BaseContainerPropsSchema.shape.style,
7 | props: z
8 | .object({
9 | childrenIds: z.array(z.string()).optional().nullable(),
10 | })
11 | .optional()
12 | .nullable(),
13 | });
14 |
15 | export type ContainerProps = z.infer;
16 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/Container/ContainerReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container as BaseContainer } from '@usewaypoint/block-container';
4 |
5 | import { ReaderBlock } from '../../Reader/core';
6 |
7 | import { ContainerProps } from './ContainerPropsSchema';
8 |
9 | export default function ContainerReader({ style, props }: ContainerProps) {
10 | const childrenIds = props?.childrenIds ?? [];
11 | return (
12 |
13 | {childrenIds.map((childId) => (
14 |
15 | ))}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const COLOR_SCHEMA = z
4 | .string()
5 | .regex(/^#[0-9a-fA-F]{6}$/)
6 | .nullable()
7 | .optional();
8 |
9 | const FONT_FAMILY_SCHEMA = z
10 | .enum([
11 | 'MODERN_SANS',
12 | 'BOOK_SANS',
13 | 'ORGANIC_SANS',
14 | 'GEOMETRIC_SANS',
15 | 'HEAVY_SANS',
16 | 'ROUNDED_SANS',
17 | 'MODERN_SERIF',
18 | 'BOOK_SERIF',
19 | 'MONOSPACE',
20 | ])
21 | .nullable()
22 | .optional();
23 |
24 | export const EmailLayoutPropsSchema = z.object({
25 | backdropColor: COLOR_SCHEMA,
26 | borderColor: COLOR_SCHEMA,
27 | borderRadius: z.number().optional().nullable(),
28 | canvasColor: COLOR_SCHEMA,
29 | textColor: COLOR_SCHEMA,
30 | fontFamily: FONT_FAMILY_SCHEMA,
31 | childrenIds: z.array(z.string()).optional().nullable(),
32 | });
33 |
34 | export type EmailLayoutProps = z.infer;
35 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ReaderBlock } from '../../Reader/core';
4 |
5 | import { EmailLayoutProps } from './EmailLayoutPropsSchema';
6 |
7 | function getFontFamily(fontFamily: EmailLayoutProps['fontFamily']) {
8 | const f = fontFamily ?? 'MODERN_SANS';
9 | switch (f) {
10 | case 'MODERN_SANS':
11 | return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif';
12 | case 'BOOK_SANS':
13 | return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif';
14 | case 'ORGANIC_SANS':
15 | return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif';
16 | case 'GEOMETRIC_SANS':
17 | return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif';
18 | case 'HEAVY_SANS':
19 | return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif';
20 | case 'ROUNDED_SANS':
21 | return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif';
22 | case 'MODERN_SERIF':
23 | return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif';
24 | case 'BOOK_SERIF':
25 | return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif';
26 | case 'MONOSPACE':
27 | return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace';
28 | }
29 | }
30 |
31 | function getBorder({ borderColor }: EmailLayoutProps) {
32 | if (!borderColor) {
33 | return undefined;
34 | }
35 | return `1px solid ${borderColor}`;
36 | }
37 |
38 | export default function EmailLayoutReader(props: EmailLayoutProps) {
39 | const childrenIds = props.childrenIds ?? [];
40 | return (
41 |
56 |
71 |
72 |
73 |
74 | {childrenIds.map((childId) => (
75 |
76 | ))}
77 |
78 |
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/packages/email-builder/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as renderToStaticMarkup } from './renderers/renderToStaticMarkup';
2 |
3 | export {
4 | ReaderBlockSchema,
5 | TReaderBlock,
6 | //
7 | ReaderDocumentSchema,
8 | TReaderDocument,
9 | //
10 | ReaderBlock,
11 | TReaderBlockProps,
12 | //
13 | TReaderProps,
14 | default as Reader,
15 | } from './Reader/core';
16 |
--------------------------------------------------------------------------------
/packages/email-builder/src/renderers/renderToStaticMarkup.spec.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | import renderToStaticMarkup from './renderToStaticMarkup';
6 |
7 | describe('renderToStaticMarkup', () => {
8 | it('renders into a string', () => {
9 | const result = renderToStaticMarkup(
10 | {
11 | root: {
12 | type: 'Container',
13 | data: {
14 | props: {
15 | childrenIds: [],
16 | },
17 | },
18 | },
19 | },
20 | { rootBlockId: 'root' }
21 | );
22 | expect(result).toEqual('
');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/email-builder/src/renderers/renderToStaticMarkup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToStaticMarkup as baseRenderToStaticMarkup } from 'react-dom/server';
3 |
4 | import Reader, { TReaderDocument } from '../Reader/core';
5 |
6 | type TOptions = {
7 | rootBlockId: string;
8 | };
9 | export default function renderToStaticMarkup(document: TReaderDocument, { rootBlockId }: TOptions) {
10 | return (
11 | '' +
12 | baseRenderToStaticMarkup(
13 |
14 |
15 |
16 |
17 |
18 | )
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/email-builder/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["tests/**/*.spec.ts", "tests/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/email-builder/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "esnext",
5 | "lib": [],
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "strict": true,
9 | "sourceMap": true,
10 | "allowJs": true,
11 | "esModuleInterop": true,
12 |
13 | "skipLibCheck": true,
14 | "declarationMap": true,
15 | "declaration": true,
16 | "noUnusedLocals": true,
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | "allowSyntheticDefaultImports": true
20 | },
21 | "exclude": ["dist"]
22 | }
23 |
--------------------------------------------------------------------------------