mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-25 07:46:53 +08:00
feat: Inject email-builder instead of loading it as ES module
- Add email-builder source - Update yarn lock
This commit is contained in:
parent
ae98280858
commit
e6f08a052c
98 changed files with 10514 additions and 172 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -2,9 +2,16 @@ frontend/node_modules/
|
|||
frontend/.cache/
|
||||
frontend/yarn.lock
|
||||
frontend/build/
|
||||
frontend/public/static/email-builder/
|
||||
frontend/dist/
|
||||
email-builder/node_modules/
|
||||
email-builder/.cache/
|
||||
email-builder/yarn.lock
|
||||
email-builder/dist/
|
||||
.vscode/
|
||||
|
||||
config.toml
|
||||
node_modules
|
||||
listmonk
|
||||
dist/*
|
||||
dist/*
|
||||
uploads/
|
||||
|
|
29
Makefile
29
Makefile
|
@ -11,13 +11,23 @@ GOPATH ?= $(HOME)/go
|
|||
STUFFBIN ?= $(GOPATH)/bin/stuffbin
|
||||
FRONTEND_YARN_MODULES = frontend/node_modules
|
||||
FRONTEND_DIST = frontend/dist
|
||||
FRONTEND_EMAIL_BUILDER_DIST = frontend/public/static/email-builder
|
||||
FRONTEND_DEPS = \
|
||||
$(FRONTEND_YARN_MODULES) \
|
||||
$(FRONTEND_EMAIL_BUILDER_DIST) \
|
||||
frontend/index.html \
|
||||
frontend/package.json \
|
||||
frontend/vite.config.js \
|
||||
frontend/.eslintrc.js \
|
||||
$(shell find frontend/fontello frontend/public frontend/src -type f)
|
||||
EMAIL_BUILDER_YARN_MODULES = email-builder/node_modules
|
||||
EMAIL_BUILDER_DIST = email-builder/dist
|
||||
EMAIL_BUILDER_DEPS = \
|
||||
$(EMAIL_BUILDER_YARN_MODULES) \
|
||||
email-builder/package.json \
|
||||
email-builder/tsconfig.json \
|
||||
email-builder/vite.config.ts \
|
||||
$(shell find email-builder/src -type f)
|
||||
|
||||
BIN := listmonk
|
||||
STATIC := config.toml.sample \
|
||||
|
@ -37,6 +47,10 @@ $(FRONTEND_YARN_MODULES): frontend/package.json frontend/yarn.lock
|
|||
cd frontend && $(YARN) install
|
||||
touch -c $(FRONTEND_YARN_MODULES)
|
||||
|
||||
$(EMAIL_BUILDER_YARN_MODULES): frontend/package.json frontend/yarn.lock
|
||||
cd email-builder && $(YARN) install
|
||||
touch -c $(EMAIL_BUILDER_YARN_MODULES)
|
||||
|
||||
# Build the backend to ./listmonk.
|
||||
$(BIN): $(shell find . -type f -name "*.go") go.mod go.sum schema.sql queries.sql permissions.json
|
||||
CGO_ENABLED=0 go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
|
||||
|
@ -51,9 +65,22 @@ $(FRONTEND_DIST): $(FRONTEND_DEPS)
|
|||
export VUE_APP_VERSION="${VERSION}" && cd frontend && $(YARN) build
|
||||
touch -c $(FRONTEND_DIST)
|
||||
|
||||
# Build the JS email-builder dist.
|
||||
$(EMAIL_BUILDER_DIST): $(EMAIL_BUILDER_DEPS)
|
||||
export VUE_APP_VERSION="${VERSION}" && cd email-builder && $(YARN) build
|
||||
touch -c $(EMAIL_BUILDER_DIST)
|
||||
|
||||
# Copy the build assets to frontend.
|
||||
$(FRONTEND_EMAIL_BUILDER_DIST): $(EMAIL_BUILDER_DIST)
|
||||
mkdir -p $(FRONTEND_EMAIL_BUILDER_DIST)
|
||||
cp -r $(EMAIL_BUILDER_DIST)/* $(FRONTEND_EMAIL_BUILDER_DIST)
|
||||
touch -c $(FRONTEND_EMAIL_BUILDER_DIST)
|
||||
|
||||
.PHONY: build-frontend
|
||||
build-frontend: $(FRONTEND_DIST)
|
||||
build-frontend: $(FRONTEND_EMAIL_BUILDER_DIST) $(FRONTEND_DIST)
|
||||
|
||||
.PHONY: build-email-builder
|
||||
build-email-builder: $(EMAIL_BUILDER_DIST) $(FRONTEND_EMAIL_BUILDER_DIST)
|
||||
|
||||
# Run the JS frontend server in dev mode.
|
||||
.PHONY: run-frontend
|
||||
|
|
21
email-builder/LICENSE
Normal file
21
email-builder/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
10
email-builder/README.md
Normal file
10
email-builder/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# @usewaypoint/editor-sample
|
||||
|
||||
Use this as a sample to self-host EmailBuilder.js.
|
||||
|
||||
To run this locally, fork the repository and then in this directory run:
|
||||
|
||||
- `npm install`
|
||||
- `npx vite`
|
||||
|
||||
Once the server is running, open http://localhost:5173/email-builder-js/ in your browser.
|
87
email-builder/index.html
Normal file
87
email-builder/index.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/src/favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/src/favicon/favicon-16x16.png" />
|
||||
<meta name="viewport" content="width=900" />
|
||||
<meta name="description" content="EmailBuilder.js interactive playground. Brought to you by Waypoint." />
|
||||
<title>EmailBuilder.js — Free and Open Source Template Builder</title>
|
||||
<style>
|
||||
html {
|
||||
margin: 0px;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
#root {
|
||||
/* height: 100vh; */
|
||||
width: 800px;
|
||||
position: relative;
|
||||
}
|
||||
.root-wrapper {
|
||||
padding: 100px;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="root-wrapper">
|
||||
<div id="root" class="email-builder-container"></div>
|
||||
</div>
|
||||
<script type="module">
|
||||
const testData = {
|
||||
"root": {
|
||||
"type": "EmailLayout",
|
||||
"data": {
|
||||
"backdropColor": "#F5F5F5",
|
||||
"canvasColor": "#FFFFFF",
|
||||
"textColor": "#262626",
|
||||
"fontFamily": "MODERN_SANS",
|
||||
"childrenIds": [
|
||||
"block-1727858083795"
|
||||
]
|
||||
}
|
||||
},
|
||||
"block-1727858083795": {
|
||||
"type": "Text",
|
||||
"data": {
|
||||
"style": {
|
||||
"fontWeight": "normal",
|
||||
"padding": {
|
||||
"top": 16,
|
||||
"bottom": 16,
|
||||
"right": 24,
|
||||
"left": 24
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"markdown": false,
|
||||
"text": "Test template"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import('/src/main.tsx')
|
||||
.then(module => {
|
||||
module.render('root', { data: testData, onChange: (json, html) => {
|
||||
console.log("onChange", json, html)
|
||||
}});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading the module:', error);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Prod build -->
|
||||
<!-- <script src="dist/listmonk-email-builder.umd.js"></script>
|
||||
<script>
|
||||
EmailBuilder.render("root");
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
50
email-builder/package.json
Normal file
50
email-builder/package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "@usewaypoint/editor-sample",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@usewaypoint/block-avatar": "^0.0.3",
|
||||
"@usewaypoint/block-button": "^0.0.3",
|
||||
"@usewaypoint/block-columns-container": "^0.0.3",
|
||||
"@usewaypoint/block-container": "^0.0.2",
|
||||
"@usewaypoint/block-divider": "^0.0.4",
|
||||
"@usewaypoint/block-heading": "^0.0.3",
|
||||
"@usewaypoint/block-html": "^0.0.3",
|
||||
"@usewaypoint/block-image": "^0.0.5",
|
||||
"@usewaypoint/block-spacer": "^0.0.3",
|
||||
"@usewaypoint/block-text": "^0.0.6",
|
||||
"@usewaypoint/document-core": "^0.0.6",
|
||||
"@usewaypoint/email-builder": "^0.0.8",
|
||||
"highlight.js": "^11.9.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"terser": "^5.34.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { TEditorBlock } from '../../../documents/editor/core';
|
||||
import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
|
||||
|
||||
import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
|
||||
import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
|
||||
import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
|
||||
import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
|
||||
import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
|
||||
import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
|
||||
import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
|
||||
import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
|
||||
import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
|
||||
import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
|
||||
import TextSidebarPanel from './input-panels/TextSidebarPanel';
|
||||
|
||||
function renderMessage(val: string) {
|
||||
return (
|
||||
<Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
|
||||
<Typography color="text.secondary">{val}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConfigurationPanel() {
|
||||
const document = useDocument();
|
||||
const selectedBlockId = useSelectedBlockId();
|
||||
|
||||
if (!selectedBlockId) {
|
||||
return renderMessage('Click on a block to inspect.');
|
||||
}
|
||||
const block = document[selectedBlockId];
|
||||
if (!block) {
|
||||
return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
|
||||
}
|
||||
|
||||
const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
|
||||
const { data, type } = block;
|
||||
switch (type) {
|
||||
case 'Avatar':
|
||||
return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Button':
|
||||
return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'ColumnsContainer':
|
||||
return (
|
||||
<ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
|
||||
);
|
||||
case 'Container':
|
||||
return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Divider':
|
||||
return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Heading':
|
||||
return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Html':
|
||||
return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Image':
|
||||
return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'EmailLayout':
|
||||
return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Spacer':
|
||||
return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Text':
|
||||
return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
default:
|
||||
return <pre>{JSON.stringify(block, null, ' ')}</pre>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { AspectRatioOutlined } from '@mui/icons-material';
|
||||
import { ToggleButton } from '@mui/material';
|
||||
import { AvatarProps, AvatarPropsDefaults, AvatarPropsSchema } from '@usewaypoint/block-avatar';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import RadioGroupInput from './helpers/inputs/RadioGroupInput';
|
||||
import SliderInput from './helpers/inputs/SliderInput';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type AvatarSidebarPanelProps = {
|
||||
data: AvatarProps;
|
||||
setData: (v: AvatarProps) => void;
|
||||
};
|
||||
export default function AvatarSidebarPanel({ data, setData }: AvatarSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = AvatarPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const size = data.props?.size ?? AvatarPropsDefaults.size;
|
||||
const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
|
||||
const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
|
||||
const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Avatar block">
|
||||
<SliderInput
|
||||
label="Size"
|
||||
iconLabel={<AspectRatioOutlined sx={{ color: 'text.secondary' }} />}
|
||||
units="px"
|
||||
step={3}
|
||||
min={32}
|
||||
max={256}
|
||||
defaultValue={size}
|
||||
onChange={(size) => {
|
||||
updateData({ ...data, props: { ...data.props, size } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Shape"
|
||||
defaultValue={shape}
|
||||
onChange={(shape) => {
|
||||
updateData({ ...data, props: { ...data.props, shape } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="circle">Circle</ToggleButton>
|
||||
<ToggleButton value="square">Square</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<TextInput
|
||||
label="Image URL"
|
||||
defaultValue={imageUrl}
|
||||
onChange={(imageUrl) => {
|
||||
updateData({ ...data, props: { ...data.props, imageUrl } });
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={alt}
|
||||
onChange={(alt) => {
|
||||
updateData({ ...data, props: { ...data.props, alt } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={['textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { ToggleButton } from '@mui/material';
|
||||
import { ButtonProps, ButtonPropsDefaults, ButtonPropsSchema } from '@usewaypoint/block-button';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import ColorInput from './helpers/inputs/ColorInput';
|
||||
import RadioGroupInput from './helpers/inputs/RadioGroupInput';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type ButtonSidebarPanelProps = {
|
||||
data: ButtonProps;
|
||||
setData: (v: ButtonProps) => void;
|
||||
};
|
||||
export default function ButtonSidebarPanel({ data, setData }: ButtonSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ButtonPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const text = data.props?.text ?? ButtonPropsDefaults.text;
|
||||
const url = data.props?.url ?? ButtonPropsDefaults.url;
|
||||
const fullWidth = data.props?.fullWidth ?? ButtonPropsDefaults.fullWidth;
|
||||
const size = data.props?.size ?? ButtonPropsDefaults.size;
|
||||
const buttonStyle = data.props?.buttonStyle ?? ButtonPropsDefaults.buttonStyle;
|
||||
const buttonTextColor = data.props?.buttonTextColor ?? ButtonPropsDefaults.buttonTextColor;
|
||||
const buttonBackgroundColor = data.props?.buttonBackgroundColor ?? ButtonPropsDefaults.buttonBackgroundColor;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Button block">
|
||||
<TextInput
|
||||
label="Text"
|
||||
defaultValue={text}
|
||||
onChange={(text) => updateData({ ...data, props: { ...data.props, text } })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Url"
|
||||
defaultValue={url}
|
||||
onChange={(url) => updateData({ ...data, props: { ...data.props, url } })}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Width"
|
||||
defaultValue={fullWidth ? 'FULL_WIDTH' : 'AUTO'}
|
||||
onChange={(v) => updateData({ ...data, props: { ...data.props, fullWidth: v === 'FULL_WIDTH' } })}
|
||||
>
|
||||
<ToggleButton value="FULL_WIDTH">Full</ToggleButton>
|
||||
<ToggleButton value="AUTO">Auto</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Size"
|
||||
defaultValue={size}
|
||||
onChange={(size) => updateData({ ...data, props: { ...data.props, size } })}
|
||||
>
|
||||
<ToggleButton value="x-small">Xs</ToggleButton>
|
||||
<ToggleButton value="small">Sm</ToggleButton>
|
||||
<ToggleButton value="medium">Md</ToggleButton>
|
||||
<ToggleButton value="large">Lg</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Style"
|
||||
defaultValue={buttonStyle}
|
||||
onChange={(buttonStyle) => updateData({ ...data, props: { ...data.props, buttonStyle } })}
|
||||
>
|
||||
<ToggleButton value="rectangle">Rectangle</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
<ToggleButton value="pill">Pill</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={buttonTextColor}
|
||||
onChange={(buttonTextColor) => updateData({ ...data, props: { ...data.props, buttonTextColor } })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Button color"
|
||||
defaultValue={buttonBackgroundColor}
|
||||
onChange={(buttonBackgroundColor) => updateData({ ...data, props: { ...data.props, buttonBackgroundColor } })}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={['backgroundColor', 'fontFamily', 'fontSize', 'fontWeight', 'textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
SpaceBarOutlined,
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import ColumnsContainerPropsSchema, {
|
||||
ColumnsContainerProps,
|
||||
} from '../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import ColumnWidthsInput from './helpers/inputs/ColumnWidthsInput';
|
||||
import RadioGroupInput from './helpers/inputs/RadioGroupInput';
|
||||
import SliderInput from './helpers/inputs/SliderInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type ColumnsContainerPanelProps = {
|
||||
data: ColumnsContainerProps;
|
||||
setData: (v: ColumnsContainerProps) => void;
|
||||
};
|
||||
export default function ColumnsContainerPanel({ data, setData }: ColumnsContainerPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ColumnsContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Columns block">
|
||||
<RadioGroupInput
|
||||
label="Number of columns"
|
||||
defaultValue={data.props?.columnsCount === 2 ? '2' : '3'}
|
||||
onChange={(v) => {
|
||||
updateData({ ...data, props: { ...data.props, columnsCount: v === '2' ? 2 : 3 } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="2">2</ToggleButton>
|
||||
<ToggleButton value="3">3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColumnWidthsInput
|
||||
defaultValue={data.props?.fixedWidths}
|
||||
onChange={(fixedWidths) => {
|
||||
updateData({ ...data, props: { ...data.props, fixedWidths } });
|
||||
}}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Columns gap"
|
||||
iconLabel={<SpaceBarOutlined sx={{ color: 'text.secondary' }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={80}
|
||||
defaultValue={data.props?.columnsGap ?? 0}
|
||||
onChange={(columnsGap) => updateData({ ...data, props: { ...data.props, columnsGap } })}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? 'middle'}
|
||||
onChange={(contentAlignment) => {
|
||||
updateData({ ...data, props: { ...data.props, contentAlignment } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={['backgroundColor', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import ContainerPropsSchema, { ContainerProps } from '../../../../documents/blocks/Container/ContainerPropsSchema';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type ContainerSidebarPanelProps = {
|
||||
data: ContainerProps;
|
||||
setData: (v: ContainerProps) => void;
|
||||
};
|
||||
|
||||
export default function ContainerSidebarPanel({ data, setData }: ContainerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<BaseSidebarPanel title="Container block">
|
||||
<MultiStylePropertyPanel
|
||||
names={['backgroundColor', 'borderColor', 'borderRadius', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { HeightOutlined } from '@mui/icons-material';
|
||||
import { DividerProps, DividerPropsDefaults, DividerPropsSchema } from '@usewaypoint/block-divider';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import ColorInput from './helpers/inputs/ColorInput';
|
||||
import SliderInput from './helpers/inputs/SliderInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type DividerSidebarPanelProps = {
|
||||
data: DividerProps;
|
||||
setData: (v: DividerProps) => void;
|
||||
};
|
||||
export default function DividerSidebarPanel({ data, setData }: DividerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = DividerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
|
||||
const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Divider block">
|
||||
<ColorInput
|
||||
label="Color"
|
||||
defaultValue={lineColor}
|
||||
onChange={(lineColor) => updateData({ ...data, props: { ...data.props, lineColor } })}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: 'text.secondary' }} />}
|
||||
units="px"
|
||||
step={1}
|
||||
min={1}
|
||||
max={24}
|
||||
defaultValue={lineHeight}
|
||||
onChange={(lineHeight) => updateData({ ...data, props: { ...data.props, lineHeight } })}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={['backgroundColor', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { RoundedCornerOutlined } from '@mui/icons-material';
|
||||
|
||||
import EmailLayoutPropsSchema, {
|
||||
EmailLayoutProps,
|
||||
} from '../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import ColorInput, { NullableColorInput } from './helpers/inputs/ColorInput';
|
||||
import { NullableFontFamily } from './helpers/inputs/FontFamily';
|
||||
import SliderInput from './helpers/inputs/SliderInput';
|
||||
|
||||
type EmailLayoutSidebarFieldsProps = {
|
||||
data: EmailLayoutProps;
|
||||
setData: (v: EmailLayoutProps) => void;
|
||||
};
|
||||
export default function EmailLayoutSidebarFields({ data, setData }: EmailLayoutSidebarFieldsProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = EmailLayoutPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Global">
|
||||
<ColorInput
|
||||
label="Backdrop color"
|
||||
defaultValue={data.backdropColor ?? '#F5F5F5'}
|
||||
onChange={(backdropColor) => updateData({ ...data, backdropColor })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Canvas color"
|
||||
defaultValue={data.canvasColor ?? '#FFFFFF'}
|
||||
onChange={(canvasColor) => updateData({ ...data, canvasColor })}
|
||||
/>
|
||||
<NullableColorInput
|
||||
label="Canvas border color"
|
||||
defaultValue={data.borderColor ?? null}
|
||||
onChange={(borderColor) => updateData({ ...data, borderColor })}
|
||||
/>
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Canvas border radius"
|
||||
defaultValue={data.borderRadius ?? 0}
|
||||
onChange={(borderRadius) => updateData({ ...data, borderRadius })}
|
||||
/>
|
||||
<NullableFontFamily
|
||||
label="Font family"
|
||||
defaultValue="MODERN_SANS"
|
||||
onChange={(fontFamily) => updateData({ ...data, fontFamily })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={data.textColor ?? '#262626'}
|
||||
onChange={(textColor) => updateData({ ...data, textColor })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { ToggleButton } from '@mui/material';
|
||||
import { HeadingProps, HeadingPropsDefaults, HeadingPropsSchema } from '@usewaypoint/block-heading';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import RadioGroupInput from './helpers/inputs/RadioGroupInput';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type HeadingSidebarPanelProps = {
|
||||
data: HeadingProps;
|
||||
setData: (v: HeadingProps) => void;
|
||||
};
|
||||
export default function HeadingSidebarPanel({ data, setData }: HeadingSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HeadingPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Heading block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={3}
|
||||
defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
|
||||
onChange={(text) => {
|
||||
updateData({ ...data, props: { ...data.props, text } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Level"
|
||||
defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
|
||||
onChange={(level) => {
|
||||
updateData({ ...data, props: { ...data.props, level } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="h1">H1</ToggleButton>
|
||||
<ToggleButton value="h2">H2</ToggleButton>
|
||||
<ToggleButton value="h3">H3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<MultiStylePropertyPanel
|
||||
names={['color', 'backgroundColor', 'fontFamily', 'fontWeight', 'textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { HtmlProps, HtmlPropsSchema } from '@usewaypoint/block-html';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type HtmlSidebarPanelProps = {
|
||||
data: HtmlProps;
|
||||
setData: (v: HtmlProps) => void;
|
||||
};
|
||||
export default function HtmlSidebarPanel({ data, setData }: HtmlSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HtmlPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Html block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.contents ?? ''}
|
||||
onChange={(contents) => updateData({ ...data, props: { ...data.props, contents } })}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={['color', 'backgroundColor', 'fontFamily', 'fontSize', 'textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { Stack, ToggleButton } from '@mui/material';
|
||||
import { ImageProps, ImagePropsSchema } from '@usewaypoint/block-image';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import RadioGroupInput from './helpers/inputs/RadioGroupInput';
|
||||
import TextDimensionInput from './helpers/inputs/TextDimensionInput';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type ImageSidebarPanelProps = {
|
||||
data: ImageProps;
|
||||
setData: (v: ImageProps) => void;
|
||||
};
|
||||
export default function ImageSidebarPanel({ data, setData }: ImageSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ImagePropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Image block">
|
||||
<TextInput
|
||||
label="Source URL"
|
||||
defaultValue={data.props?.url ?? ''}
|
||||
onChange={(v) => {
|
||||
const url = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, url } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={data.props?.alt ?? ''}
|
||||
onChange={(alt) => updateData({ ...data, props: { ...data.props, alt } })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Click through URL"
|
||||
defaultValue={data.props?.linkHref ?? ''}
|
||||
onChange={(v) => {
|
||||
const linkHref = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, linkHref } });
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextDimensionInput
|
||||
label="Width"
|
||||
defaultValue={data.props?.width}
|
||||
onChange={(width) => updateData({ ...data, props: { ...data.props, width } })}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Height"
|
||||
defaultValue={data.props?.height}
|
||||
onChange={(height) => updateData({ ...data, props: { ...data.props, height } })}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? 'middle'}
|
||||
onChange={(contentAlignment) => updateData({ ...data, props: { ...data.props, contentAlignment } })}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={['backgroundColor', 'textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { HeightOutlined } from '@mui/icons-material';
|
||||
import { SpacerProps, SpacerPropsDefaults, SpacerPropsSchema } from '@usewaypoint/block-spacer';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import SliderInput from './helpers/inputs/SliderInput';
|
||||
|
||||
type SpacerSidebarPanelProps = {
|
||||
data: SpacerProps;
|
||||
setData: (v: SpacerProps) => void;
|
||||
};
|
||||
export default function SpacerSidebarPanel({ data, setData }: SpacerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = SpacerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Spacer block">
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: 'text.secondary' }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
min={4}
|
||||
max={128}
|
||||
defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
|
||||
onChange={(height) => updateData({ ...data, props: { ...data.props, height } })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { TextProps, TextPropsSchema } from '@usewaypoint/block-text';
|
||||
|
||||
import BaseSidebarPanel from './helpers/BaseSidebarPanel';
|
||||
import BooleanInput from './helpers/inputs/BooleanInput';
|
||||
import TextInput from './helpers/inputs/TextInput';
|
||||
import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
|
||||
|
||||
type TextSidebarPanelProps = {
|
||||
data: TextProps;
|
||||
setData: (v: TextProps) => void;
|
||||
};
|
||||
export default function TextSidebarPanel({ data, setData }: TextSidebarPanelProps) {
|
||||
const [, setErrors] = useState<Zod.ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = TextPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Text block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.text ?? ''}
|
||||
onChange={(text) => updateData({ ...data, props: { ...data.props, text } })}
|
||||
/>
|
||||
<BooleanInput
|
||||
label="Markdown"
|
||||
defaultValue={data.props?.markdown ?? false}
|
||||
onChange={(markdown) => updateData({ ...data, props: { ...data.props, markdown } })}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={['color', 'backgroundColor', 'fontFamily', 'fontSize', 'fontWeight', 'textAlign', 'padding']}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
|
||||
type SidebarPanelProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export default function BaseSidebarPanel({ title, children }: SidebarPanelProps) {
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Typography variant="overline" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack spacing={5} mb={3}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { FormControlLabel, Switch } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export default function BooleanInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<FormControlLabel
|
||||
label={label}
|
||||
control={
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(_, checked: boolean) => {
|
||||
setValue(checked);
|
||||
onChange(checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { AddOutlined, CloseOutlined } from '@mui/icons-material';
|
||||
import { ButtonBase, InputLabel, Menu, Stack } from '@mui/material';
|
||||
|
||||
import Picker from './Picker';
|
||||
|
||||
const BUTTON_SX = {
|
||||
border: '1px solid',
|
||||
borderColor: 'cadet.400',
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '4px',
|
||||
bgcolor: '#FFFFFF',
|
||||
};
|
||||
|
||||
type Props =
|
||||
| {
|
||||
nullable: true;
|
||||
label: string;
|
||||
onChange: (value: string | null) => void;
|
||||
defaultValue: string | null;
|
||||
}
|
||||
| {
|
||||
nullable: false;
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput({ label, defaultValue, onChange, nullable }: Props) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const renderResetButton = () => {
|
||||
if (!nullable) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setValue(null);
|
||||
onChange(null);
|
||||
}}
|
||||
>
|
||||
<CloseOutlined fontSize="small" sx={{ color: 'grey.600' }} />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOpenButton = () => {
|
||||
if (value) {
|
||||
return <ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX, bgcolor: value }} />;
|
||||
}
|
||||
return (
|
||||
<ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
|
||||
<AddOutlined fontSize="small" />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{renderOpenButton()}
|
||||
{renderResetButton()}
|
||||
</Stack>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
MenuListProps={{
|
||||
sx: { height: 'auto', padding: 0 },
|
||||
}}
|
||||
>
|
||||
<Picker
|
||||
value={value || ''}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { HexColorInput, HexColorPicker } from 'react-colorful';
|
||||
|
||||
import { Box, Stack, SxProps } from '@mui/material';
|
||||
|
||||
import Swatch from './Swatch';
|
||||
|
||||
const DEFAULT_PRESET_COLORS = [
|
||||
'#E11D48',
|
||||
'#DB2777',
|
||||
'#C026D3',
|
||||
'#9333EA',
|
||||
'#7C3AED',
|
||||
'#4F46E5',
|
||||
'#2563EB',
|
||||
'#0284C7',
|
||||
'#0891B2',
|
||||
'#0D9488',
|
||||
'#059669',
|
||||
'#16A34A',
|
||||
'#65A30D',
|
||||
'#CA8A04',
|
||||
'#D97706',
|
||||
'#EA580C',
|
||||
'#DC2626',
|
||||
'#FFFFFF',
|
||||
'#FAFAFA',
|
||||
'#F5F5F5',
|
||||
'#E5E5E5',
|
||||
'#D4D4D4',
|
||||
'#A3A3A3',
|
||||
'#737373',
|
||||
'#525252',
|
||||
'#404040',
|
||||
'#262626',
|
||||
'#171717',
|
||||
'#0A0A0A',
|
||||
'#000000',
|
||||
];
|
||||
|
||||
const SX: SxProps = {
|
||||
p: 1,
|
||||
'.react-colorful__pointer ': {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
'.react-colorful__saturation': {
|
||||
mb: 1,
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'.react-colorful__last-control': {
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'.react-colorful__hue-pointer': {
|
||||
width: '4px',
|
||||
borderRadius: '4px',
|
||||
height: 24,
|
||||
cursor: 'col-resize',
|
||||
},
|
||||
'.react-colorful__saturation-pointer': {
|
||||
cursor: 'all-scroll',
|
||||
},
|
||||
input: {
|
||||
padding: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
borderRadius: '4px',
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function Picker({ value, onChange }: Props) {
|
||||
return (
|
||||
<Stack spacing={1} sx={SX}>
|
||||
<HexColorPicker color={value} onChange={onChange} />
|
||||
<Swatch paletteColors={DEFAULT_PRESET_COLORS} value={value} onChange={onChange} />
|
||||
<Box pt={1}>
|
||||
<HexColorInput prefixed color={value} onChange={onChange} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Button, SxProps } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
paletteColors: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const TILE_BUTTON: SxProps = {
|
||||
width: 24,
|
||||
height: 24,
|
||||
};
|
||||
export default function Swatch({ paletteColors, value, onChange }: Props) {
|
||||
const renderButton = (colorValue: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={colorValue}
|
||||
onClick={() => onChange(colorValue)}
|
||||
sx={{
|
||||
...TILE_BUTTON,
|
||||
backgroundColor: colorValue,
|
||||
border: '1px solid',
|
||||
borderColor: value === colorValue ? 'black' : 'grey.200',
|
||||
minWidth: 24,
|
||||
display: 'inline-flex',
|
||||
'&:hover': {
|
||||
backgroundColor: colorValue,
|
||||
borderColor: 'grey.500',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box width="100%" sx={{ display: 'grid', gap: 1, gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr 1fr' }}>
|
||||
{paletteColors.map((c) => renderButton(c))}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
import BaseColorInput from './BaseColorInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput(props: Props) {
|
||||
return <BaseColorInput {...props} nullable={false} />;
|
||||
}
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableColorInput(props: NullableProps) {
|
||||
return <BaseColorInput {...props} nullable />;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
import TextDimensionInput from './TextDimensionInput';
|
||||
|
||||
export const DEFAULT_2_COLUMNS = [6] as [number];
|
||||
export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
|
||||
|
||||
type TWidthValue = number | null | undefined;
|
||||
type FixedWidths = [
|
||||
//
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
];
|
||||
type ColumnsLayoutInputProps = {
|
||||
defaultValue: FixedWidths | null | undefined;
|
||||
onChange: (v: FixedWidths | null | undefined) => void;
|
||||
};
|
||||
export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
|
||||
const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return [null, null, null];
|
||||
});
|
||||
|
||||
const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
|
||||
const nValue: FixedWidths = [...currentValue];
|
||||
nValue[index] = value;
|
||||
setCurrentValue(nValue);
|
||||
onChange(nValue);
|
||||
};
|
||||
|
||||
const columnsCountValue = 3;
|
||||
let column3 = null;
|
||||
if (columnsCountValue === 3) {
|
||||
column3 = (
|
||||
<TextDimensionInput
|
||||
label="Column 3"
|
||||
defaultValue={currentValue?.[2]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(2, v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextDimensionInput
|
||||
label="Column 1"
|
||||
defaultValue={currentValue?.[0]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(0, v);
|
||||
}}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Column 2"
|
||||
defaultValue={currentValue?.[1]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(1, v);
|
||||
}}
|
||||
/>
|
||||
{column3}
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { MenuItem, TextField } from '@mui/material';
|
||||
|
||||
import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
|
||||
|
||||
const OPTIONS = FONT_FAMILIES.map((option) => (
|
||||
<MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'inherit');
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
variant="standard"
|
||||
label={label}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v === null ? null : v);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="inherit">Match email settings</MenuItem>
|
||||
{OPTIONS}
|
||||
</TextField>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { TextFieldsOutlined } from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleChange = (value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value}
|
||||
setValue={handleChange}
|
||||
units="px"
|
||||
step={1}
|
||||
min={10}
|
||||
max={48}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(fontWeight) => {
|
||||
setValue(fontWeight);
|
||||
onChange(fontWeight);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="normal">Regular</ToggleButton>
|
||||
<ToggleButton value="bold">Bold</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
AlignHorizontalLeftOutlined,
|
||||
AlignHorizontalRightOutlined,
|
||||
AlignVerticalBottomOutlined,
|
||||
AlignVerticalTopOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type TPaddingValue = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
left: number;
|
||||
};
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: TPaddingValue | null;
|
||||
onChange: (value: TPaddingValue) => void;
|
||||
};
|
||||
export default function PaddingInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(internalName: keyof TPaddingValue, nValue: number) {
|
||||
const v = {
|
||||
...value,
|
||||
[internalName]: nValue,
|
||||
};
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={2} alignItems="flex-start" pb={1}>
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.top}
|
||||
setValue={(num) => handleChange('top', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.bottom}
|
||||
setValue={(num) => handleChange('bottom', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.left}
|
||||
setValue={(num) => handleChange('left', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.right}
|
||||
setValue={(num) => handleChange('right', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string | JSX.Element;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
fullWidth
|
||||
value={value}
|
||||
size="small"
|
||||
onChange={(_, v: unknown) => {
|
||||
if (typeof v !== 'string') {
|
||||
throw new Error('RadioGroupInput can only receive string values');
|
||||
}
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type SliderInputProps = {
|
||||
label: string;
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
value={value}
|
||||
setValue={(value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
};
|
||||
export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'left');
|
||||
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="left">
|
||||
<FormatAlignLeftOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="center">
|
||||
<FormatAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="right">
|
||||
<FormatAlignRightOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TextField, Typography } from '@mui/material';
|
||||
|
||||
type TextDimensionInputProps = {
|
||||
label: string;
|
||||
defaultValue: number | null | undefined;
|
||||
onChange: (v: number | null) => void;
|
||||
};
|
||||
export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = parseInt(ev.target.value);
|
||||
onChange(isNaN(value) ? null : value);
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
defaultValue={defaultValue}
|
||||
label={label}
|
||||
variant="standard"
|
||||
placeholder="auto"
|
||||
size="small"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
px
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { InputProps, TextField } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
helperText?: string | JSX.Element;
|
||||
InputProps?: InputProps;
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const isMultiline = typeof rows === 'number' && rows > 1;
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline={isMultiline}
|
||||
minRows={rows}
|
||||
variant={isMultiline ? 'outlined' : 'standard'}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
helperText={helperText}
|
||||
InputProps={InputProps}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Slider, Stack, Typography } from '@mui/material';
|
||||
|
||||
type SliderInputProps = {
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
value: number;
|
||||
setValue: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function RawSliderInput({ iconLabel, value, setValue, units, ...props }: SliderInputProps) {
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" spacing={2} justifyContent="space-between" width="100%">
|
||||
<Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
|
||||
<Slider
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={(_, value: unknown) => {
|
||||
if (typeof value !== 'number') {
|
||||
throw new Error('RawSliderInput values can only receive numeric values');
|
||||
}
|
||||
setValue(value);
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 32, textAlign: 'right', flexShrink: 0 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1 }}>
|
||||
{value}
|
||||
{units}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TStyle } from '../../../../../../documents/blocks/helpers/TStyle';
|
||||
|
||||
import SingleStylePropertyPanel from './SingleStylePropertyPanel';
|
||||
|
||||
type MultiStylePropertyPanelProps = {
|
||||
names: (keyof TStyle)[];
|
||||
value: TStyle | undefined | null;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function MultiStylePropertyPanel({ names, value, onChange }: MultiStylePropertyPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{names.map((name) => (
|
||||
<SingleStylePropertyPanel key={name} name={name} value={value || {}} onChange={onChange} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
|
||||
import { RoundedCornerOutlined } from '@mui/icons-material';
|
||||
|
||||
import { TStyle } from '../../../../../../documents/blocks/helpers/TStyle';
|
||||
import { NullableColorInput } from '../inputs/ColorInput';
|
||||
import { NullableFontFamily } from '../inputs/FontFamily';
|
||||
import FontSizeInput from '../inputs/FontSizeInput';
|
||||
import FontWeightInput from '../inputs/FontWeightInput';
|
||||
import PaddingInput from '../inputs/PaddingInput';
|
||||
import SliderInput from '../inputs/SliderInput';
|
||||
import TextAlignInput from '../inputs/TextAlignInput';
|
||||
|
||||
type StylePropertyPanelProps = {
|
||||
name: keyof TStyle;
|
||||
value: TStyle;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function SingleStylePropertyPanel({ name, value, onChange }: StylePropertyPanelProps) {
|
||||
const defaultValue = value[name] ?? null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleChange = (v: any) => {
|
||||
onChange({ ...value, [name]: v });
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case 'backgroundColor':
|
||||
return <NullableColorInput label="Background color" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'borderColor':
|
||||
return <NullableColorInput label="Border color" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'borderRadius':
|
||||
return (
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Border radius"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
return <NullableColorInput label="Text color" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'fontFamily':
|
||||
return <NullableFontFamily label="Font family" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'fontSize':
|
||||
return <FontSizeInput label="Font size" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'fontWeight':
|
||||
return <FontWeightInput label="Font weight" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'textAlign':
|
||||
return <TextAlignInput label="Alignment" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
case 'padding':
|
||||
return <PaddingInput label="Padding" defaultValue={defaultValue} onChange={handleChange} />;
|
||||
}
|
||||
}
|
19
email-builder/src/App/InspectorDrawer/StylesPanel.tsx
Normal file
19
email-builder/src/App/InspectorDrawer/StylesPanel.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { setDocument, useDocument } from '../../documents/editor/EditorContext';
|
||||
|
||||
import EmailLayoutSidebarPanel from './ConfigurationPanel/input-panels/EmailLayoutSidebarPanel';
|
||||
|
||||
export default function StylesPanel() {
|
||||
const block = useDocument().root;
|
||||
if (!block) {
|
||||
return <p>Block not found</p>;
|
||||
}
|
||||
|
||||
const { data, type } = block;
|
||||
if (type !== 'EmailLayout') {
|
||||
throw new Error('Expected "root" element to be of type EmailLayout');
|
||||
}
|
||||
|
||||
return <EmailLayoutSidebarPanel key="root" data={data} setData={(data) => setDocument({ root: { type, data } })} />;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
|
||||
import { AppRegistrationOutlined, LastPageOutlined } from '@mui/icons-material';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
import { toggleInspectorDrawerOpen, useInspectorDrawerOpen } from '../../documents/editor/EditorContext';
|
||||
|
||||
export default function ToggleInspectorPanelButton() {
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const handleClick = () => {
|
||||
toggleInspectorDrawerOpen();
|
||||
};
|
||||
if (inspectorDrawerOpen) {
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<LastPageOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<AppRegistrationOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
53
email-builder/src/App/InspectorDrawer/index.tsx
Normal file
53
email-builder/src/App/InspectorDrawer/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Drawer, Tab, Tabs } from '@mui/material';
|
||||
|
||||
import { setSidebarTab, useInspectorDrawerOpen, useSelectedSidebarTab } from '../../documents/editor/EditorContext';
|
||||
|
||||
import ConfigurationPanel from './ConfigurationPanel';
|
||||
import StylesPanel from './StylesPanel';
|
||||
|
||||
export const INSPECTOR_DRAWER_WIDTH = 320;
|
||||
|
||||
export default function InspectorDrawer() {
|
||||
const selectedSidebarTab = useSelectedSidebarTab();
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const renderCurrentSidebarPanel = () => {
|
||||
switch (selectedSidebarTab) {
|
||||
case 'block-configuration':
|
||||
return <ConfigurationPanel />;
|
||||
case 'styles':
|
||||
return <StylesPanel />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="right"
|
||||
open={inspectorDrawerOpen}
|
||||
sx={{
|
||||
width: inspectorDrawerOpen ? INSPECTOR_DRAWER_WIDTH : 0,
|
||||
}}
|
||||
// Make the drawer relative to the wrapper instead of body.
|
||||
PaperProps={{ style: { position: 'absolute', zIndex: 0 } }}
|
||||
ModalProps={{
|
||||
container: document.querySelector('.email-builder-container'),
|
||||
style: { position: 'absolute', zIndex: 0 }
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: INSPECTOR_DRAWER_WIDTH, height: 49, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box px={2}>
|
||||
<Tabs value={selectedSidebarTab} onChange={(_, v) => setSidebarTab(v)}>
|
||||
<Tab value="styles" label="Styles" />
|
||||
<Tab value="block-configuration" label="Inspect" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ width: INSPECTOR_DRAWER_WIDTH, height: 'calc(100% - 49px)', overflow: 'auto' }}>
|
||||
{renderCurrentSidebarPanel()}
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
20
email-builder/src/App/TemplatePanel/DownloadJson/index.tsx
Normal file
20
email-builder/src/App/TemplatePanel/DownloadJson/index.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { FileDownloadOutlined } from '@mui/icons-material';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
|
||||
import { useDocument } from '../../../documents/editor/EditorContext';
|
||||
|
||||
export default function DownloadJson() {
|
||||
const doc = useDocument();
|
||||
const href = useMemo(() => {
|
||||
return `data:text/plain,${encodeURIComponent(JSON.stringify(doc, null, ' '))}`;
|
||||
}, [doc]);
|
||||
return (
|
||||
<Tooltip title="Download JSON file">
|
||||
<IconButton href={href} download="emailTemplate.json">
|
||||
<FileDownloadOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
13
email-builder/src/App/TemplatePanel/HtmlPanel.tsx
Normal file
13
email-builder/src/App/TemplatePanel/HtmlPanel.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { renderToStaticMarkup } from '@usewaypoint/email-builder';
|
||||
|
||||
import { useDocument } from '../../documents/editor/EditorContext';
|
||||
|
||||
import HighlightedCodePanel from './helper/HighlightedCodePanel';
|
||||
|
||||
export default function HtmlPanel() {
|
||||
const document = useDocument();
|
||||
const code = useMemo(() => renderToStaticMarkup(document, { rootBlockId: 'root' }), [document]);
|
||||
return <HighlightedCodePanel type="html" value={code} />;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Link,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
import { resetDocument } from '../../../documents/editor/EditorContext';
|
||||
|
||||
import validateJsonStringValue from './validateJsonStringValue';
|
||||
|
||||
type ImportJsonDialogProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
export default function ImportJsonDialog({ onClose }: ImportJsonDialogProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (ev) => {
|
||||
const v = ev.currentTarget.value;
|
||||
setValue(v);
|
||||
const { error } = validateJsonStringValue(v);
|
||||
setError(error ?? null);
|
||||
};
|
||||
|
||||
let errorAlert = null;
|
||||
if (error) {
|
||||
errorAlert = <Alert color="error">{error}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose}>
|
||||
<DialogTitle>Import JSON</DialogTitle>
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
ev.preventDefault();
|
||||
const { error, data } = validateJsonStringValue(value);
|
||||
setError(error ?? null);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
resetDocument(data);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography color="text.secondary" paragraph>
|
||||
Copy and paste an EmailBuilder.js JSON (
|
||||
<Link
|
||||
href="https://gist.githubusercontent.com/jordanisip/efb61f56ba71bd36d3a9440122cb7f50/raw/30ea74a6ac7e52ebdc309bce07b71a9286ce2526/emailBuilderTemplate.json"
|
||||
target="_blank"
|
||||
underline="none"
|
||||
>
|
||||
example
|
||||
</Link>
|
||||
).
|
||||
</Typography>
|
||||
{errorAlert}
|
||||
<TextField
|
||||
error={error !== null}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
type="text"
|
||||
helperText="This will override your current template."
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
rows={10}
|
||||
multiline
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" type="submit" disabled={error !== null}>
|
||||
Import
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
26
email-builder/src/App/TemplatePanel/ImportJson/index.tsx
Normal file
26
email-builder/src/App/TemplatePanel/ImportJson/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { FileUploadOutlined } from '@mui/icons-material';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
|
||||
import ImportJsonDialog from './ImportJsonDialog';
|
||||
|
||||
export default function ImportJson() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
let dialog = null;
|
||||
if (open) {
|
||||
dialog = <ImportJsonDialog onClose={() => setOpen(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Import JSON">
|
||||
<IconButton onClick={() => setOpen(true)}>
|
||||
<FileUploadOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{dialog}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { EditorConfigurationSchema, TEditorConfiguration } from '../../../documents/editor/core';
|
||||
|
||||
type TResult = { error: string; data?: undefined } | { data: TEditorConfiguration; error?: undefined };
|
||||
|
||||
export default function validateTextAreaValue(value: string): TResult {
|
||||
let jsonObject = undefined;
|
||||
try {
|
||||
jsonObject = JSON.parse(value);
|
||||
} catch {
|
||||
return { error: 'Invalid json' };
|
||||
}
|
||||
|
||||
const parseResult = EditorConfigurationSchema.safeParse(jsonObject);
|
||||
if (!parseResult.success) {
|
||||
return { error: 'Invalid JSON schema' };
|
||||
}
|
||||
|
||||
if (!parseResult.data.root) {
|
||||
return { error: 'Missing "root" node' };
|
||||
}
|
||||
|
||||
return { data: parseResult.data };
|
||||
}
|
11
email-builder/src/App/TemplatePanel/JsonPanel.tsx
Normal file
11
email-builder/src/App/TemplatePanel/JsonPanel.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useDocument } from '../../documents/editor/EditorContext';
|
||||
|
||||
import HighlightedCodePanel from './helper/HighlightedCodePanel';
|
||||
|
||||
export default function JsonPanel() {
|
||||
const document = useDocument();
|
||||
const code = useMemo(() => JSON.stringify(document, null, ' '), [document]);
|
||||
return <HighlightedCodePanel type="json" value={code} />;
|
||||
}
|
59
email-builder/src/App/TemplatePanel/MainTabsGroup.tsx
Normal file
59
email-builder/src/App/TemplatePanel/MainTabsGroup.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
|
||||
import { CodeOutlined, DataObjectOutlined, EditOutlined, PreviewOutlined } from '@mui/icons-material';
|
||||
import { Tab, Tabs, Tooltip } from '@mui/material';
|
||||
|
||||
import { setSelectedMainTab, useSelectedMainTab } from '../../documents/editor/EditorContext';
|
||||
|
||||
export default function MainTabsGroup() {
|
||||
const selectedMainTab = useSelectedMainTab();
|
||||
const handleChange = (_: unknown, v: unknown) => {
|
||||
switch (v) {
|
||||
case 'json':
|
||||
case 'preview':
|
||||
case 'editor':
|
||||
case 'html':
|
||||
setSelectedMainTab(v);
|
||||
return;
|
||||
default:
|
||||
setSelectedMainTab('editor');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs value={selectedMainTab} onChange={handleChange}>
|
||||
<Tab
|
||||
value="editor"
|
||||
label={
|
||||
<Tooltip title="Edit">
|
||||
<EditOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
value="preview"
|
||||
label={
|
||||
<Tooltip title="Preview">
|
||||
<PreviewOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
value="html"
|
||||
label={
|
||||
<Tooltip title="HTML output">
|
||||
<CodeOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<Tab
|
||||
value="json"
|
||||
label={
|
||||
<Tooltip title="JSON output">
|
||||
<DataObjectOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
37
email-builder/src/App/TemplatePanel/ShareButton.tsx
Normal file
37
email-builder/src/App/TemplatePanel/ShareButton.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { IosShareOutlined } from '@mui/icons-material';
|
||||
import { IconButton, Snackbar, Tooltip } from '@mui/material';
|
||||
|
||||
import { useDocument } from '../../documents/editor/EditorContext';
|
||||
|
||||
export default function ShareButton() {
|
||||
const document = useDocument();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const onClick = async () => {
|
||||
const c = encodeURIComponent(JSON.stringify(document));
|
||||
location.hash = `#code/${btoa(c)}`;
|
||||
setMessage('The URL was updated. Copy it to share your current template.');
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setMessage(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={onClick}>
|
||||
<Tooltip title="Share current template">
|
||||
<IosShareOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
open={message !== null}
|
||||
onClose={onClose}
|
||||
message={message}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { html, json } from './highlighters';
|
||||
|
||||
type TextEditorPanelProps = {
|
||||
type: 'json' | 'html' | 'javascript';
|
||||
value: string;
|
||||
};
|
||||
export default function HighlightedCodePanel({ type, value }: TextEditorPanelProps) {
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
switch (type) {
|
||||
case 'html':
|
||||
html(value).then(setCode);
|
||||
return;
|
||||
case 'json':
|
||||
json(value).then(setCode);
|
||||
return;
|
||||
}
|
||||
}, [setCode, value, type]);
|
||||
|
||||
if (code === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
style={{ margin: 0, padding: 16, height: '100%', overflow: 'auto' }}
|
||||
dangerouslySetInnerHTML={{ __html: code }}
|
||||
onClick={(ev) => {
|
||||
const s = window.getSelection();
|
||||
if (s === null) {
|
||||
return;
|
||||
}
|
||||
s.selectAllChildren(ev.currentTarget);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
28
email-builder/src/App/TemplatePanel/helper/highlighters.tsx
Normal file
28
email-builder/src/App/TemplatePanel/helper/highlighters.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import hljs from 'highlight.js';
|
||||
import jsonHighlighter from 'highlight.js/lib/languages/json';
|
||||
import xmlHighlighter from 'highlight.js/lib/languages/xml';
|
||||
import prettierPluginBabel from 'prettier/plugins/babel';
|
||||
import prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import prettierPluginHtml from 'prettier/plugins/html';
|
||||
import { format } from 'prettier/standalone';
|
||||
|
||||
hljs.registerLanguage('json', jsonHighlighter);
|
||||
hljs.registerLanguage('html', xmlHighlighter);
|
||||
|
||||
export async function html(value: string): Promise<string> {
|
||||
const prettyValue = await format(value, {
|
||||
parser: 'html',
|
||||
plugins: [prettierPluginHtml],
|
||||
});
|
||||
return hljs.highlight(prettyValue, { language: 'html' }).value;
|
||||
}
|
||||
|
||||
export async function json(value: string): Promise<string> {
|
||||
const prettyValue = await format(value, {
|
||||
parser: 'json',
|
||||
printWidth: 0,
|
||||
trailingComma: 'all',
|
||||
plugins: [prettierPluginBabel, prettierPluginEstree],
|
||||
});
|
||||
return hljs.highlight(prettyValue, { language: 'javascript' }).value;
|
||||
}
|
115
email-builder/src/App/TemplatePanel/index.tsx
Normal file
115
email-builder/src/App/TemplatePanel/index.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import React from 'react';
|
||||
|
||||
import { MonitorOutlined, PhoneIphoneOutlined } from '@mui/icons-material';
|
||||
import { Box, Stack, SxProps, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material';
|
||||
import { Reader } from '@usewaypoint/email-builder';
|
||||
|
||||
import EditorBlock from '../../documents/editor/EditorBlock';
|
||||
import {
|
||||
setSelectedScreenSize,
|
||||
useDocument,
|
||||
useSelectedMainTab,
|
||||
useSelectedScreenSize,
|
||||
} from '../../documents/editor/EditorContext';
|
||||
import ToggleInspectorPanelButton from '../InspectorDrawer/ToggleInspectorPanelButton';
|
||||
|
||||
import DownloadJson from './DownloadJson';
|
||||
import HtmlPanel from './HtmlPanel';
|
||||
import ImportJson from './ImportJson';
|
||||
import JsonPanel from './JsonPanel';
|
||||
import MainTabsGroup from './MainTabsGroup';
|
||||
|
||||
export default function TemplatePanel() {
|
||||
const document = useDocument();
|
||||
const selectedMainTab = useSelectedMainTab();
|
||||
const selectedScreenSize = useSelectedScreenSize();
|
||||
|
||||
let mainBoxSx: SxProps = {
|
||||
height: '100%',
|
||||
};
|
||||
if (selectedScreenSize === 'mobile') {
|
||||
mainBoxSx = {
|
||||
...mainBoxSx,
|
||||
margin: '32px auto',
|
||||
width: 370,
|
||||
height: 800,
|
||||
boxShadow:
|
||||
'rgba(33, 36, 67, 0.04) 0px 10px 20px, rgba(33, 36, 67, 0.04) 0px 2px 6px, rgba(33, 36, 67, 0.04) 0px 0px 1px',
|
||||
};
|
||||
}
|
||||
|
||||
const handleScreenSizeChange = (_: unknown, value: unknown) => {
|
||||
switch (value) {
|
||||
case 'mobile':
|
||||
case 'desktop':
|
||||
setSelectedScreenSize(value);
|
||||
return;
|
||||
default:
|
||||
setSelectedScreenSize('desktop');
|
||||
}
|
||||
};
|
||||
|
||||
const renderMainPanel = () => {
|
||||
switch (selectedMainTab) {
|
||||
case 'editor':
|
||||
return (
|
||||
<Box sx={mainBoxSx}>
|
||||
<EditorBlock id="root" />
|
||||
</Box>
|
||||
);
|
||||
case 'preview':
|
||||
return (
|
||||
<Box sx={mainBoxSx}>
|
||||
<Reader document={document} rootBlockId="root" />
|
||||
</Box>
|
||||
);
|
||||
case 'html':
|
||||
return <HtmlPanel />;
|
||||
case 'json':
|
||||
return <JsonPanel />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
height: 49,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
backgroundColor: 'white',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
px: 1,
|
||||
}}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Stack px={2} direction="row" gap={2} width="100%" justifyContent="space-between" alignItems="center">
|
||||
<Stack direction="row" spacing={2}>
|
||||
<MainTabsGroup />
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<DownloadJson />
|
||||
<ImportJson />
|
||||
<ToggleButtonGroup value={selectedScreenSize} exclusive size="small" onChange={handleScreenSizeChange}>
|
||||
<ToggleButton value="desktop">
|
||||
<Tooltip title="Desktop view">
|
||||
<MonitorOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="mobile">
|
||||
<Tooltip title="Mobile view">
|
||||
<PhoneIphoneOutlined fontSize="small" />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ToggleInspectorPanelButton />
|
||||
</Stack>
|
||||
<Box sx={{ height: 'calc(100vh - 49px)', overflow: 'auto', minWidth: 370 }}>{renderMainPanel()}</Box>
|
||||
</>
|
||||
);
|
||||
}
|
68
email-builder/src/App/index.tsx
Normal file
68
email-builder/src/App/index.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { Stack, useTheme } from '@mui/material';
|
||||
import { renderToStaticMarkup } from '@usewaypoint/email-builder';
|
||||
|
||||
import { TEditorConfiguration } from '../documents/editor/core';
|
||||
import { useInspectorDrawerOpen, useSamplesDrawerOpen, subscribeDocument, setDocument } from '../documents/editor/EditorContext';
|
||||
import InspectorDrawer, { INSPECTOR_DRAWER_WIDTH } from './InspectorDrawer';
|
||||
import TemplatePanel from './TemplatePanel';
|
||||
|
||||
export const DEFAULT_SOURCE: TEditorConfiguration = {
|
||||
"root": {
|
||||
"type": "EmailLayout",
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
|
||||
function useDrawerTransition(cssProperty: 'margin-left' | 'margin-right', open: boolean) {
|
||||
const { transitions } = useTheme();
|
||||
return transitions.create(cssProperty, {
|
||||
easing: !open ? transitions.easing.sharp : transitions.easing.easeOut,
|
||||
duration: !open ? transitions.duration.leavingScreen : transitions.duration.enteringScreen,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AppProps {
|
||||
// Initial configuration to load. Optional.
|
||||
data?: TEditorConfiguration,
|
||||
// Callback for any change in document. Optional.
|
||||
onChange?: (json: TEditorConfiguration, html: String) => void,
|
||||
// Optional height for the Stack component.
|
||||
height?: string,
|
||||
}
|
||||
|
||||
export default function App(props: AppProps) {
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
const samplesDrawerOpen = useSamplesDrawerOpen();
|
||||
|
||||
const marginLeftTransition = useDrawerTransition('margin-left', samplesDrawerOpen);
|
||||
const marginRightTransition = useDrawerTransition('margin-right', inspectorDrawerOpen);
|
||||
|
||||
if (props.data) {
|
||||
setDocument(props.data)
|
||||
} else {
|
||||
setDocument(DEFAULT_SOURCE)
|
||||
}
|
||||
|
||||
if (props.onChange) {
|
||||
subscribeDocument ((document) => {
|
||||
props.onChange?.(document, renderToStaticMarkup(document, { rootBlockId: 'root' }))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InspectorDrawer />
|
||||
|
||||
<Stack
|
||||
sx={{
|
||||
marginRight: inspectorDrawerOpen ? `${INSPECTOR_DRAWER_WIDTH}px` : 0,
|
||||
transition: [marginLeftTransition, marginRightTransition].join(', '),
|
||||
height: props.height ? props.height : 'auto',
|
||||
}}
|
||||
>
|
||||
<TemplatePanel />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container';
|
||||
|
||||
import { useCurrentBlockId } from '../../editor/EditorBlock';
|
||||
import { setDocument, setSelectedBlockId } from '../../editor/EditorContext';
|
||||
import EditorChildrenIds, { EditorChildrenChange } from '../helpers/EditorChildrenIds';
|
||||
|
||||
import ColumnsContainerPropsSchema, { ColumnsContainerProps } from './ColumnsContainerPropsSchema';
|
||||
|
||||
const EMPTY_COLUMNS = [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }];
|
||||
|
||||
export default function ColumnsContainerEditor({ style, props }: ColumnsContainerProps) {
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
const { columns, ...restProps } = props ?? {};
|
||||
const columnsValue = columns ?? EMPTY_COLUMNS;
|
||||
|
||||
const updateColumn = (columnIndex: 0 | 1 | 2, { block, blockId, childrenIds }: EditorChildrenChange) => {
|
||||
const nColumns = [...columnsValue];
|
||||
nColumns[columnIndex] = { childrenIds };
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: 'ColumnsContainer',
|
||||
data: ColumnsContainerPropsSchema.parse({
|
||||
style,
|
||||
props: {
|
||||
...restProps,
|
||||
columns: nColumns,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseColumnsContainer
|
||||
props={restProps}
|
||||
style={style}
|
||||
columns={[
|
||||
<EditorChildrenIds childrenIds={columns?.[0]?.childrenIds} onChange={(change) => updateColumn(0, change)} />,
|
||||
<EditorChildrenIds childrenIds={columns?.[1]?.childrenIds} onChange={(change) => updateColumn(1, change)} />,
|
||||
<EditorChildrenIds childrenIds={columns?.[2]?.childrenIds} onChange={(change) => updateColumn(2, change)} />,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container';
|
||||
|
||||
const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape;
|
||||
|
||||
const ColumnsContainerPropsSchema = z.object({
|
||||
style: BaseColumnsContainerPropsSchema.shape.style,
|
||||
props: z
|
||||
.object({
|
||||
...BasePropsShape,
|
||||
columns: z.tuple([
|
||||
z.object({ childrenIds: z.array(z.string()) }),
|
||||
z.object({ childrenIds: z.array(z.string()) }),
|
||||
z.object({ childrenIds: z.array(z.string()) }),
|
||||
]),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type ColumnsContainerProps = z.infer<typeof ColumnsContainerPropsSchema>;
|
||||
export default ColumnsContainerPropsSchema;
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Container as BaseContainer } from '@usewaypoint/block-container';
|
||||
|
||||
import { useCurrentBlockId } from '../../editor/EditorBlock';
|
||||
import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext';
|
||||
import EditorChildrenIds from '../helpers/EditorChildrenIds';
|
||||
|
||||
import { ContainerProps } from './ContainerPropsSchema';
|
||||
|
||||
export default function ContainerEditor({ style, props }: ContainerProps) {
|
||||
const childrenIds = props?.childrenIds ?? [];
|
||||
|
||||
const document = useDocument();
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
return (
|
||||
<BaseContainer style={style}>
|
||||
<EditorChildrenIds
|
||||
childrenIds={childrenIds}
|
||||
onChange={({ block, blockId, childrenIds }) => {
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
...document[currentBlockId].data,
|
||||
props: { childrenIds: childrenIds },
|
||||
},
|
||||
},
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
}}
|
||||
/>
|
||||
</BaseContainer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container';
|
||||
|
||||
const ContainerPropsSchema = z.object({
|
||||
style: BaseContainerPropsSchema.shape.style,
|
||||
props: z
|
||||
.object({
|
||||
childrenIds: z.array(z.string()).optional().nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export default ContainerPropsSchema;
|
||||
|
||||
export type ContainerProps = z.infer<typeof ContainerPropsSchema>;
|
|
@ -0,0 +1,103 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useCurrentBlockId } from '../../editor/EditorBlock';
|
||||
import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext';
|
||||
import EditorChildrenIds from '../helpers/EditorChildrenIds';
|
||||
|
||||
import { EmailLayoutProps } from './EmailLayoutPropsSchema';
|
||||
|
||||
function getFontFamily(fontFamily: EmailLayoutProps['fontFamily']) {
|
||||
const f = fontFamily ?? 'MODERN_SANS';
|
||||
switch (f) {
|
||||
case 'MODERN_SANS':
|
||||
return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif';
|
||||
case 'BOOK_SANS':
|
||||
return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif';
|
||||
case 'ORGANIC_SANS':
|
||||
return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif';
|
||||
case 'GEOMETRIC_SANS':
|
||||
return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif';
|
||||
case 'HEAVY_SANS':
|
||||
return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif';
|
||||
case 'ROUNDED_SANS':
|
||||
return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif';
|
||||
case 'MODERN_SERIF':
|
||||
return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif';
|
||||
case 'BOOK_SERIF':
|
||||
return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif';
|
||||
case 'MONOSPACE':
|
||||
return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace';
|
||||
}
|
||||
}
|
||||
|
||||
export default function EmailLayoutEditor(props: EmailLayoutProps) {
|
||||
const childrenIds = props.childrenIds ?? [];
|
||||
const document = useDocument();
|
||||
const currentBlockId = useCurrentBlockId();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedBlockId(null);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: props.backdropColor ?? '#F5F5F5',
|
||||
color: props.textColor ?? '#262626',
|
||||
fontFamily: getFontFamily(props.fontFamily),
|
||||
fontSize: '16px',
|
||||
fontWeight: '400',
|
||||
letterSpacing: '0.15008px',
|
||||
lineHeight: '1.5',
|
||||
margin: '0',
|
||||
padding: '32px 0',
|
||||
width: '100%',
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
maxWidth: '600px',
|
||||
backgroundColor: props.canvasColor ?? '#FFFFFF',
|
||||
borderRadius: props.borderRadius ?? undefined,
|
||||
border: (() => {
|
||||
const v = props.borderColor;
|
||||
if (!v) {
|
||||
return undefined;
|
||||
}
|
||||
return `1px solid ${v}`;
|
||||
})(),
|
||||
}}
|
||||
role="presentation"
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
border={0}
|
||||
>
|
||||
<tbody>
|
||||
<tr style={{ width: '100%' }}>
|
||||
<td>
|
||||
<EditorChildrenIds
|
||||
childrenIds={childrenIds}
|
||||
onChange={({ block, blockId, childrenIds }) => {
|
||||
setDocument({
|
||||
[blockId]: block,
|
||||
[currentBlockId]: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
...document[currentBlockId].data,
|
||||
childrenIds: childrenIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
setSelectedBlockId(blockId);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const COLOR_SCHEMA = z
|
||||
.string()
|
||||
.regex(/^#[0-9a-fA-F]{6}$/)
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
const FONT_FAMILY_SCHEMA = z
|
||||
.enum([
|
||||
'MODERN_SANS',
|
||||
'BOOK_SANS',
|
||||
'ORGANIC_SANS',
|
||||
'GEOMETRIC_SANS',
|
||||
'HEAVY_SANS',
|
||||
'ROUNDED_SANS',
|
||||
'MODERN_SERIF',
|
||||
'BOOK_SERIF',
|
||||
'MONOSPACE',
|
||||
])
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
const EmailLayoutPropsSchema = z.object({
|
||||
backdropColor: COLOR_SCHEMA,
|
||||
borderColor: COLOR_SCHEMA,
|
||||
borderRadius: z.number().optional().nullable(),
|
||||
canvasColor: COLOR_SCHEMA,
|
||||
textColor: COLOR_SCHEMA,
|
||||
fontFamily: FONT_FAMILY_SCHEMA,
|
||||
childrenIds: z.array(z.string()).optional().nullable(),
|
||||
});
|
||||
|
||||
export default EmailLayoutPropsSchema;
|
||||
|
||||
export type EmailLayoutProps = z.infer<typeof EmailLayoutPropsSchema>;
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Button, SxProps, Typography } from '@mui/material';
|
||||
|
||||
type BlockMenuButtonProps = {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const BUTTON_SX: SxProps = { p: 1.5, display: 'flex', flexDirection: 'column' };
|
||||
const ICON_SX: SxProps = {
|
||||
mb: 0.75,
|
||||
width: '100%',
|
||||
bgcolor: 'cadet.200',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
p: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'cadet.300',
|
||||
};
|
||||
|
||||
export default function BlockTypeButton({ label, icon, onClick }: BlockMenuButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
sx={BUTTON_SX}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Box sx={ICON_SX}>{icon}</Box>
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Box, Menu } from '@mui/material';
|
||||
|
||||
import { TEditorBlock } from '../../../../editor/core';
|
||||
|
||||
import BlockButton from './BlockButton';
|
||||
import { BUTTONS } from './buttons';
|
||||
|
||||
type BlocksMenuProps = {
|
||||
anchorEl: HTMLElement | null;
|
||||
setAnchorEl: (v: HTMLElement | null) => void;
|
||||
onSelect: (block: TEditorBlock) => void;
|
||||
};
|
||||
export default function BlocksMenu({ anchorEl, setAnchorEl, onSelect }: BlocksMenuProps) {
|
||||
const onClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onClick = (block: TEditorBlock) => {
|
||||
onSelect(block);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
if (anchorEl === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<Box sx={{ p: 1, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr' }}>
|
||||
{BUTTONS.map((k, i) => (
|
||||
<BlockButton key={i} label={k.label} icon={k.icon} onClick={() => onClick(k.block())} />
|
||||
))}
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { AddOutlined } from '@mui/icons-material';
|
||||
import { Fade, IconButton } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
buttonElement: HTMLElement | null;
|
||||
onClick: () => void;
|
||||
};
|
||||
export default function DividerButton({ buttonElement, onClick }: Props) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function listener({ clientX, clientY }: MouseEvent) {
|
||||
if (!buttonElement) {
|
||||
return;
|
||||
}
|
||||
const rect = buttonElement.getBoundingClientRect();
|
||||
const rectY = rect.y;
|
||||
const bottomX = rect.x;
|
||||
const topX = bottomX + rect.width;
|
||||
|
||||
if (Math.abs(clientY - rectY) < 20) {
|
||||
if (bottomX < clientX && clientX < topX) {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
}
|
||||
window.addEventListener('mousemove', listener);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', listener);
|
||||
};
|
||||
}, [buttonElement, setVisible]);
|
||||
|
||||
return (
|
||||
<Fade in={visible}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
p: 0.12,
|
||||
position: 'absolute',
|
||||
top: '-12px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-10px)',
|
||||
bgcolor: 'brand.blue',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover, &:active, &:focus': {
|
||||
bgcolor: 'brand.blue',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<AddOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Fade>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
import { AddOutlined } from '@mui/icons-material';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
};
|
||||
export default function PlaceholderButton({ onClick }: Props) {
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
width: '100%',
|
||||
bgcolor: 'rgba(0,0,0, 0.05)',
|
||||
}}
|
||||
>
|
||||
<AddOutlined
|
||||
sx={{
|
||||
p: 0.12,
|
||||
bgcolor: 'brand.blue',
|
||||
borderRadius: 24,
|
||||
color: 'primary.contrastText',
|
||||
}}
|
||||
fontSize="small"
|
||||
/>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
AccountCircleOutlined,
|
||||
Crop32Outlined,
|
||||
HMobiledataOutlined,
|
||||
HorizontalRuleOutlined,
|
||||
HtmlOutlined,
|
||||
ImageOutlined,
|
||||
LibraryAddOutlined,
|
||||
NotesOutlined,
|
||||
SmartButtonOutlined,
|
||||
ViewColumnOutlined,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { TEditorBlock } from '../../../../editor/core';
|
||||
|
||||
type TButtonProps = {
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
block: () => TEditorBlock;
|
||||
};
|
||||
export const BUTTONS: TButtonProps[] = [
|
||||
{
|
||||
label: 'Heading',
|
||||
icon: <HMobiledataOutlined />,
|
||||
block: () => ({
|
||||
type: 'Heading',
|
||||
data: {
|
||||
props: { text: 'Hello friend' },
|
||||
style: {
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Text',
|
||||
icon: <NotesOutlined />,
|
||||
block: () => ({
|
||||
type: 'Text',
|
||||
data: {
|
||||
props: { text: 'My new text block' },
|
||||
style: {
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 },
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Button',
|
||||
icon: <SmartButtonOutlined />,
|
||||
block: () => ({
|
||||
type: 'Button',
|
||||
data: {
|
||||
props: {
|
||||
text: 'Button',
|
||||
url: 'https://www.usewaypoint.com',
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Image',
|
||||
icon: <ImageOutlined />,
|
||||
block: () => ({
|
||||
type: 'Image',
|
||||
data: {
|
||||
props: {
|
||||
url: 'https://assets.usewaypoint.com/sample-image.jpg',
|
||||
alt: 'Sample product',
|
||||
contentAlignment: 'middle',
|
||||
linkHref: null,
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Avatar',
|
||||
icon: <AccountCircleOutlined />,
|
||||
block: () => ({
|
||||
type: 'Avatar',
|
||||
data: {
|
||||
props: {
|
||||
imageUrl: 'https://ui-avatars.com/api/?size=128',
|
||||
shape: 'circle',
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Divider',
|
||||
icon: <HorizontalRuleOutlined />,
|
||||
block: () => ({
|
||||
type: 'Divider',
|
||||
data: {
|
||||
style: { padding: { top: 16, right: 0, bottom: 16, left: 0 } },
|
||||
props: {
|
||||
lineColor: '#CCCCCC',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Spacer',
|
||||
icon: <Crop32Outlined />,
|
||||
block: () => ({
|
||||
type: 'Spacer',
|
||||
data: {},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Html',
|
||||
icon: <HtmlOutlined />,
|
||||
block: () => ({
|
||||
type: 'Html',
|
||||
data: {
|
||||
props: { contents: '<strong>Hello world</strong>' },
|
||||
style: {
|
||||
fontSize: 16,
|
||||
textAlign: null,
|
||||
padding: { top: 16, bottom: 16, left: 24, right: 24 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Columns',
|
||||
icon: <ViewColumnOutlined />,
|
||||
block: () => ({
|
||||
type: 'ColumnsContainer',
|
||||
data: {
|
||||
props: {
|
||||
columnsGap: 16,
|
||||
columnsCount: 3,
|
||||
columns: [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }],
|
||||
},
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Container',
|
||||
icon: <LibraryAddOutlined />,
|
||||
block: () => ({
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: { padding: { top: 16, bottom: 16, left: 24, right: 24 } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
// { label: 'ProgressBar', icon: <ProgressBarOutlined />, block: () => ({}) },
|
||||
// { label: 'LoopContainer', icon: <ViewListOutlined />, block: () => ({}) },
|
||||
];
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { TEditorBlock } from '../../../../editor/core';
|
||||
|
||||
import BlocksMenu from './BlocksMenu';
|
||||
import DividerButton from './DividerButton';
|
||||
import PlaceholderButton from './PlaceholderButton';
|
||||
|
||||
type Props = {
|
||||
placeholder?: boolean;
|
||||
onSelect: (block: TEditorBlock) => void;
|
||||
};
|
||||
export default function AddBlockButton({ onSelect, placeholder }: Props) {
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [buttonElement, setButtonElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
setMenuAnchorEl(buttonElement);
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
if (placeholder) {
|
||||
return <PlaceholderButton onClick={handleButtonClick} />;
|
||||
} else {
|
||||
return <DividerButton buttonElement={buttonElement} onClick={handleButtonClick} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={setButtonElement} style={{ position: 'relative' }}>
|
||||
{renderButton()}
|
||||
</div>
|
||||
<BlocksMenu anchorEl={menuAnchorEl} setAnchorEl={setMenuAnchorEl} onSelect={onSelect} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React, { Fragment } from 'react';
|
||||
|
||||
import { TEditorBlock } from '../../../editor/core';
|
||||
import EditorBlock from '../../../editor/EditorBlock';
|
||||
|
||||
import AddBlockButton from './AddBlockMenu';
|
||||
|
||||
export type EditorChildrenChange = {
|
||||
blockId: string;
|
||||
block: TEditorBlock;
|
||||
childrenIds: string[];
|
||||
};
|
||||
|
||||
function generateId() {
|
||||
return `block-${Date.now()}`;
|
||||
}
|
||||
|
||||
export type EditorChildrenIdsProps = {
|
||||
childrenIds: string[] | null | undefined;
|
||||
onChange: (val: EditorChildrenChange) => void;
|
||||
};
|
||||
export default function EditorChildrenIds({ childrenIds, onChange }: EditorChildrenIdsProps) {
|
||||
const appendBlock = (block: TEditorBlock) => {
|
||||
const blockId = generateId();
|
||||
return onChange({
|
||||
blockId,
|
||||
block,
|
||||
childrenIds: [...(childrenIds || []), blockId],
|
||||
});
|
||||
};
|
||||
|
||||
const insertBlock = (block: TEditorBlock, index: number) => {
|
||||
const blockId = generateId();
|
||||
const newChildrenIds = [...(childrenIds || [])];
|
||||
newChildrenIds.splice(index, 0, blockId);
|
||||
return onChange({
|
||||
blockId,
|
||||
block,
|
||||
childrenIds: newChildrenIds,
|
||||
});
|
||||
};
|
||||
|
||||
if (!childrenIds || childrenIds.length === 0) {
|
||||
return <AddBlockButton placeholder onSelect={appendBlock} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{childrenIds.map((childId, i) => (
|
||||
<Fragment key={childId}>
|
||||
<AddBlockButton onSelect={(block) => insertBlock(block, i)} />
|
||||
<EditorBlock id={childId} />
|
||||
</Fragment>
|
||||
))}
|
||||
<AddBlockButton onSelect={appendBlock} />
|
||||
</>
|
||||
);
|
||||
}
|
13
email-builder/src/documents/blocks/helpers/TStyle.ts
Normal file
13
email-builder/src/documents/blocks/helpers/TStyle.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export type TStyle = {
|
||||
backgroundColor?: any;
|
||||
borderColor?: any;
|
||||
borderRadius?: any;
|
||||
color?: any;
|
||||
fontFamily?: any;
|
||||
fontSize?: any;
|
||||
fontWeight?: any;
|
||||
padding?: any;
|
||||
textAlign?: any;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import React, { CSSProperties, useState } from 'react';
|
||||
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
import { useCurrentBlockId } from '../../../editor/EditorBlock';
|
||||
import { setSelectedBlockId, useSelectedBlockId } from '../../../editor/EditorContext';
|
||||
|
||||
import TuneMenu from './TuneMenu';
|
||||
|
||||
type TEditorBlockWrapperProps = {
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export default function EditorBlockWrapper({ children }: TEditorBlockWrapperProps) {
|
||||
const selectedBlockId = useSelectedBlockId();
|
||||
const [mouseInside, setMouseInside] = useState(false);
|
||||
const blockId = useCurrentBlockId();
|
||||
|
||||
let outline: CSSProperties['outline'];
|
||||
if (selectedBlockId === blockId) {
|
||||
outline = '2px solid rgba(0,121,204, 1)';
|
||||
} else if (mouseInside) {
|
||||
outline = '2px solid rgba(0,121,204, 0.3)';
|
||||
}
|
||||
|
||||
const renderMenu = () => {
|
||||
if (selectedBlockId !== blockId) {
|
||||
return null;
|
||||
}
|
||||
return <TuneMenu blockId={blockId} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
maxWidth: '100%',
|
||||
outlineOffset: '-1px',
|
||||
outline,
|
||||
}}
|
||||
onMouseEnter={(ev) => {
|
||||
setMouseInside(true);
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setMouseInside(false);
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
setSelectedBlockId(blockId);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
{renderMenu()}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
|
||||
import { TStyle } from '../TStyle';
|
||||
|
||||
type TReaderBlockWrapperProps = {
|
||||
style: TStyle;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export default function ReaderBlockWrapper({ style, children }: TReaderBlockWrapperProps) {
|
||||
const { padding, borderColor, ...restStyle } = style;
|
||||
const cssStyle: CSSProperties = {
|
||||
...restStyle,
|
||||
};
|
||||
|
||||
if (padding) {
|
||||
const { top, bottom, left, right } = padding;
|
||||
cssStyle.padding = `${top}px ${right}px ${bottom}px ${left}px`;
|
||||
}
|
||||
|
||||
if (borderColor) {
|
||||
cssStyle.border = `1px solid ${borderColor}`;
|
||||
}
|
||||
|
||||
return <div style={{ maxWidth: '100%', ...cssStyle }}>{children}</div>;
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ArrowDownwardOutlined, ArrowUpwardOutlined, DeleteOutlined } from '@mui/icons-material';
|
||||
import { IconButton, Paper, Stack, SxProps, Tooltip } from '@mui/material';
|
||||
|
||||
import { TEditorBlock } from '../../../editor/core';
|
||||
import { resetDocument, setSelectedBlockId, useDocument } from '../../../editor/EditorContext';
|
||||
import { ColumnsContainerProps } from '../../ColumnsContainer/ColumnsContainerPropsSchema';
|
||||
|
||||
const sx: SxProps = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: -56,
|
||||
borderRadius: 64,
|
||||
paddingX: 0.5,
|
||||
paddingY: 1
|
||||
};
|
||||
|
||||
type Props = {
|
||||
blockId: string;
|
||||
};
|
||||
export default function TuneMenu({ blockId }: Props) {
|
||||
const document = useDocument();
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
const filterChildrenIds = (childrenIds: string[] | null | undefined) => {
|
||||
if (!childrenIds) {
|
||||
return childrenIds;
|
||||
}
|
||||
return childrenIds.filter((f) => f !== blockId);
|
||||
};
|
||||
const nDocument: typeof document = { ...document };
|
||||
for (const [id, b] of Object.entries(nDocument)) {
|
||||
const block = b as TEditorBlock;
|
||||
if (id === blockId) {
|
||||
continue;
|
||||
}
|
||||
switch (block.type) {
|
||||
case 'EmailLayout':
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
childrenIds: filterChildrenIds(block.data.childrenIds),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'Container':
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
props: {
|
||||
...block.data.props,
|
||||
childrenIds: filterChildrenIds(block.data.props?.childrenIds),
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'ColumnsContainer':
|
||||
nDocument[id] = {
|
||||
type: 'ColumnsContainer',
|
||||
data: {
|
||||
style: block.data.style,
|
||||
props: {
|
||||
...block.data.props,
|
||||
columns: block.data.props?.columns?.map((c) => ({
|
||||
childrenIds: filterChildrenIds(c.childrenIds),
|
||||
})),
|
||||
},
|
||||
} as ColumnsContainerProps,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
nDocument[id] = block;
|
||||
}
|
||||
}
|
||||
delete nDocument[blockId];
|
||||
resetDocument(nDocument);
|
||||
};
|
||||
|
||||
const handleMoveClick = (direction: 'up' | 'down') => {
|
||||
const moveChildrenIds = (ids: string[] | null | undefined) => {
|
||||
if (!ids) {
|
||||
return ids;
|
||||
}
|
||||
const index = ids.indexOf(blockId);
|
||||
if (index < 0) {
|
||||
return ids;
|
||||
}
|
||||
const childrenIds = [...ids];
|
||||
if (direction === 'up' && index > 0) {
|
||||
[childrenIds[index], childrenIds[index - 1]] = [childrenIds[index - 1], childrenIds[index]];
|
||||
} else if (direction === 'down' && index < childrenIds.length - 1) {
|
||||
[childrenIds[index], childrenIds[index + 1]] = [childrenIds[index + 1], childrenIds[index]];
|
||||
}
|
||||
return childrenIds;
|
||||
};
|
||||
const nDocument: typeof document = { ...document };
|
||||
for (const [id, b] of Object.entries(nDocument)) {
|
||||
const block = b as TEditorBlock;
|
||||
if (id === blockId) {
|
||||
continue;
|
||||
}
|
||||
switch (block.type) {
|
||||
case 'EmailLayout':
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
childrenIds: moveChildrenIds(block.data.childrenIds),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'Container':
|
||||
nDocument[id] = {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
props: {
|
||||
...block.data.props,
|
||||
childrenIds: moveChildrenIds(block.data.props?.childrenIds),
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
case 'ColumnsContainer':
|
||||
nDocument[id] = {
|
||||
type: 'ColumnsContainer',
|
||||
data: {
|
||||
style: block.data.style,
|
||||
props: {
|
||||
...block.data.props,
|
||||
columns: block.data.props?.columns?.map((c) => ({
|
||||
childrenIds: moveChildrenIds(c.childrenIds),
|
||||
})),
|
||||
},
|
||||
} as ColumnsContainerProps,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
nDocument[id] = block;
|
||||
}
|
||||
}
|
||||
|
||||
resetDocument(nDocument);
|
||||
setSelectedBlockId(blockId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={sx} onClick={(ev) => ev.stopPropagation()}>
|
||||
<Stack>
|
||||
<Tooltip title="Move up" placement="left-start">
|
||||
<IconButton onClick={() => handleMoveClick('up')} sx={{ color: 'text.primary' }}>
|
||||
<ArrowUpwardOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Move down" placement="left-start">
|
||||
<IconButton onClick={() => handleMoveClick('down')} sx={{ color: 'text.primary' }}>
|
||||
<ArrowDownwardOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete" placement="left-start">
|
||||
<IconButton onClick={handleDeleteClick} sx={{ color: 'text.primary' }}>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
61
email-builder/src/documents/blocks/helpers/fontFamily.ts
Normal file
61
email-builder/src/documents/blocks/helpers/fontFamily.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
export const FONT_FAMILIES = [
|
||||
{
|
||||
key: 'MODERN_SANS',
|
||||
label: 'Modern sans',
|
||||
value: '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'BOOK_SANS',
|
||||
label: 'Book sans',
|
||||
value: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'ORGANIC_SANS',
|
||||
label: 'Organic sans',
|
||||
value: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'GEOMETRIC_SANS',
|
||||
label: 'Geometric sans',
|
||||
value: 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'HEAVY_SANS',
|
||||
label: 'Heavy sans',
|
||||
value:
|
||||
'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'ROUNDED_SANS',
|
||||
label: 'Rounded sans',
|
||||
value:
|
||||
'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif',
|
||||
},
|
||||
{
|
||||
key: 'MODERN_SERIF',
|
||||
label: 'Modern serif',
|
||||
value: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif',
|
||||
},
|
||||
{
|
||||
key: 'BOOK_SERIF',
|
||||
label: 'Book serif',
|
||||
value: '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif',
|
||||
},
|
||||
{
|
||||
key: 'MONOSPACE',
|
||||
label: 'Monospace',
|
||||
value: '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace',
|
||||
},
|
||||
];
|
||||
|
||||
export const FONT_FAMILY_NAMES = [
|
||||
'MODERN_SANS',
|
||||
'BOOK_SANS',
|
||||
'ORGANIC_SANS',
|
||||
'GEOMETRIC_SANS',
|
||||
'HEAVY_SANS',
|
||||
'ROUNDED_SANS',
|
||||
'MODERN_SERIF',
|
||||
'BOOK_SERIF',
|
||||
'MONOSPACE',
|
||||
] as const;
|
28
email-builder/src/documents/blocks/helpers/zod.ts
Normal file
28
email-builder/src/documents/blocks/helpers/zod.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { FONT_FAMILY_NAMES } from './fontFamily';
|
||||
|
||||
export function zColor() {
|
||||
return z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
||||
}
|
||||
|
||||
export function zFontFamily() {
|
||||
return z.enum(FONT_FAMILY_NAMES);
|
||||
}
|
||||
|
||||
export function zFontWeight() {
|
||||
return z.enum(['bold', 'normal']);
|
||||
}
|
||||
|
||||
export function zTextAlign() {
|
||||
return z.enum(['left', 'center', 'right']);
|
||||
}
|
||||
|
||||
export function zPadding() {
|
||||
return z.object({
|
||||
top: z.number(),
|
||||
bottom: z.number(),
|
||||
right: z.number(),
|
||||
left: z.number(),
|
||||
});
|
||||
}
|
29
email-builder/src/documents/editor/EditorBlock.tsx
Normal file
29
email-builder/src/documents/editor/EditorBlock.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
import { EditorBlock as CoreEditorBlock } from './core';
|
||||
import { useDocument } from './EditorContext';
|
||||
|
||||
const EditorBlockContext = createContext<string | null>(null);
|
||||
export const useCurrentBlockId = () => useContext(EditorBlockContext)!;
|
||||
|
||||
type EditorBlockProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id - Block id
|
||||
* @returns EditorBlock component that loads data from the EditorDocumentContext
|
||||
*/
|
||||
export default function EditorBlock({ id }: EditorBlockProps) {
|
||||
const document = useDocument();
|
||||
const block = document[id];
|
||||
if (!block) {
|
||||
throw new Error('Could not find block');
|
||||
}
|
||||
return (
|
||||
<EditorBlockContext.Provider value={id}>
|
||||
<CoreEditorBlock {...block} />
|
||||
</EditorBlockContext.Provider>
|
||||
);
|
||||
}
|
114
email-builder/src/documents/editor/EditorContext.tsx
Normal file
114
email-builder/src/documents/editor/EditorContext.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
import getConfiguration from '../../getConfiguration';
|
||||
|
||||
import { TEditorConfiguration } from './core';
|
||||
|
||||
type TValue = {
|
||||
document: TEditorConfiguration;
|
||||
|
||||
selectedBlockId: string | null;
|
||||
selectedSidebarTab: 'block-configuration' | 'styles';
|
||||
selectedMainTab: 'editor' | 'preview' | 'json' | 'html';
|
||||
selectedScreenSize: 'desktop' | 'mobile';
|
||||
|
||||
inspectorDrawerOpen: boolean;
|
||||
samplesDrawerOpen: boolean;
|
||||
};
|
||||
|
||||
const editorStateStore = create(subscribeWithSelector<TValue>(() => ({
|
||||
document: getConfiguration(window.location.hash),
|
||||
selectedBlockId: null,
|
||||
selectedSidebarTab: 'styles',
|
||||
selectedMainTab: 'editor',
|
||||
selectedScreenSize: 'desktop',
|
||||
|
||||
inspectorDrawerOpen: true,
|
||||
samplesDrawerOpen: true,
|
||||
})));
|
||||
|
||||
export function useDocument() {
|
||||
return editorStateStore((s) => s.document);
|
||||
}
|
||||
|
||||
export function subscribeDocument (listener: (selectedState: TEditorConfiguration, previousSelectedState: TEditorConfiguration) => void) {
|
||||
editorStateStore.subscribe((state) => state.document, listener)
|
||||
}
|
||||
|
||||
export function useSelectedBlockId() {
|
||||
return editorStateStore((s) => s.selectedBlockId);
|
||||
}
|
||||
|
||||
export function useSelectedScreenSize() {
|
||||
return editorStateStore((s) => s.selectedScreenSize);
|
||||
}
|
||||
|
||||
export function useSelectedMainTab() {
|
||||
return editorStateStore((s) => s.selectedMainTab);
|
||||
}
|
||||
|
||||
export function setSelectedMainTab(selectedMainTab: TValue['selectedMainTab']) {
|
||||
return editorStateStore.setState({ selectedMainTab });
|
||||
}
|
||||
|
||||
export function useSelectedSidebarTab() {
|
||||
return editorStateStore((s) => s.selectedSidebarTab);
|
||||
}
|
||||
|
||||
export function useInspectorDrawerOpen() {
|
||||
return editorStateStore((s) => s.inspectorDrawerOpen);
|
||||
}
|
||||
|
||||
export function useSamplesDrawerOpen() {
|
||||
return editorStateStore((s) => s.samplesDrawerOpen);
|
||||
}
|
||||
|
||||
export function setSelectedBlockId(selectedBlockId: TValue['selectedBlockId']) {
|
||||
const selectedSidebarTab = selectedBlockId === null ? 'styles' : 'block-configuration';
|
||||
const options: Partial<TValue> = {};
|
||||
if (selectedBlockId !== null) {
|
||||
options.inspectorDrawerOpen = true;
|
||||
}
|
||||
return editorStateStore.setState({
|
||||
selectedBlockId,
|
||||
selectedSidebarTab,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function setSidebarTab(selectedSidebarTab: TValue['selectedSidebarTab']) {
|
||||
return editorStateStore.setState({ selectedSidebarTab });
|
||||
}
|
||||
|
||||
export function resetDocument(document: TValue['document']) {
|
||||
return editorStateStore.setState({
|
||||
document,
|
||||
selectedSidebarTab: 'styles',
|
||||
selectedBlockId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function setDocument(document: TValue['document']) {
|
||||
const originalDocument = editorStateStore.getState().document;
|
||||
return editorStateStore.setState({
|
||||
document: {
|
||||
...originalDocument,
|
||||
...document,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleInspectorDrawerOpen() {
|
||||
const inspectorDrawerOpen = !editorStateStore.getState().inspectorDrawerOpen;
|
||||
return editorStateStore.setState({ inspectorDrawerOpen });
|
||||
}
|
||||
|
||||
export function toggleSamplesDrawerOpen() {
|
||||
const samplesDrawerOpen = !editorStateStore.getState().samplesDrawerOpen;
|
||||
return editorStateStore.setState({ samplesDrawerOpen });
|
||||
}
|
||||
|
||||
export function setSelectedScreenSize(selectedScreenSize: TValue['selectedScreenSize']) {
|
||||
return editorStateStore.setState({ selectedScreenSize });
|
||||
}
|
127
email-builder/src/documents/editor/core.tsx
Normal file
127
email-builder/src/documents/editor/core.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Avatar, AvatarPropsSchema } from '@usewaypoint/block-avatar';
|
||||
import { Button, ButtonPropsSchema } from '@usewaypoint/block-button';
|
||||
import { Divider, DividerPropsSchema } from '@usewaypoint/block-divider';
|
||||
import { Heading, HeadingPropsSchema } from '@usewaypoint/block-heading';
|
||||
import { Html, HtmlPropsSchema } from '@usewaypoint/block-html';
|
||||
import { Image, ImagePropsSchema } from '@usewaypoint/block-image';
|
||||
import { Spacer, SpacerPropsSchema } from '@usewaypoint/block-spacer';
|
||||
import { Text, TextPropsSchema } from '@usewaypoint/block-text';
|
||||
import {
|
||||
buildBlockComponent,
|
||||
buildBlockConfigurationDictionary,
|
||||
buildBlockConfigurationSchema,
|
||||
} from '@usewaypoint/document-core';
|
||||
|
||||
import ColumnsContainerEditor from '../blocks/ColumnsContainer/ColumnsContainerEditor';
|
||||
import ColumnsContainerPropsSchema from '../blocks/ColumnsContainer/ColumnsContainerPropsSchema';
|
||||
import ContainerEditor from '../blocks/Container/ContainerEditor';
|
||||
import ContainerPropsSchema from '../blocks/Container/ContainerPropsSchema';
|
||||
import EmailLayoutEditor from '../blocks/EmailLayout/EmailLayoutEditor';
|
||||
import EmailLayoutPropsSchema from '../blocks/EmailLayout/EmailLayoutPropsSchema';
|
||||
import EditorBlockWrapper from '../blocks/helpers/block-wrappers/EditorBlockWrapper';
|
||||
|
||||
const EDITOR_DICTIONARY = buildBlockConfigurationDictionary({
|
||||
Avatar: {
|
||||
schema: AvatarPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Avatar {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Button: {
|
||||
schema: ButtonPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Button {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Container: {
|
||||
schema: ContainerPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<ContainerEditor {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
ColumnsContainer: {
|
||||
schema: ColumnsContainerPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<ColumnsContainerEditor {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Heading: {
|
||||
schema: HeadingPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Heading {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Html: {
|
||||
schema: HtmlPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Html {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Image: {
|
||||
schema: ImagePropsSchema,
|
||||
Component: (data) => {
|
||||
const props = {
|
||||
...data,
|
||||
props: {
|
||||
...data.props,
|
||||
url: data.props?.url ?? 'https://placehold.co/600x400@2x/F8F8F8/CCC?text=Your%20image',
|
||||
},
|
||||
};
|
||||
return (
|
||||
<EditorBlockWrapper>
|
||||
<Image {...props} />
|
||||
</EditorBlockWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
Text: {
|
||||
schema: TextPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Text {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
EmailLayout: {
|
||||
schema: EmailLayoutPropsSchema,
|
||||
Component: (p) => <EmailLayoutEditor {...p} />,
|
||||
},
|
||||
Spacer: {
|
||||
schema: SpacerPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Spacer {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
Divider: {
|
||||
schema: DividerPropsSchema,
|
||||
Component: (props) => (
|
||||
<EditorBlockWrapper>
|
||||
<Divider {...props} />
|
||||
</EditorBlockWrapper>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const EditorBlock = buildBlockComponent(EDITOR_DICTIONARY);
|
||||
export const EditorBlockSchema = buildBlockConfigurationSchema(EDITOR_DICTIONARY);
|
||||
export const EditorConfigurationSchema = z.record(z.string(), EditorBlockSchema);
|
||||
|
||||
export type TEditorBlock = z.infer<typeof EditorBlockSchema>;
|
||||
export type TEditorConfiguration = Record<string, TEditorBlock>;
|
BIN
email-builder/src/favicon/android-chrome-192x192.png
Normal file
BIN
email-builder/src/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
email-builder/src/favicon/android-chrome-512x512.png
Normal file
BIN
email-builder/src/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
email-builder/src/favicon/apple-touch-icon.png
Normal file
BIN
email-builder/src/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
email-builder/src/favicon/favicon-16x16.png
Normal file
BIN
email-builder/src/favicon/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 415 B |
BIN
email-builder/src/favicon/favicon-32x32.png
Normal file
BIN
email-builder/src/favicon/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 722 B |
BIN
email-builder/src/favicon/favicon.ico
Normal file
BIN
email-builder/src/favicon/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
45
email-builder/src/getConfiguration/index.tsx
Normal file
45
email-builder/src/getConfiguration/index.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import EMPTY_EMAIL_MESSAGE from './sample/empty-email-message';
|
||||
import ONE_TIME_PASSCODE from './sample/one-time-passcode';
|
||||
import ORDER_ECOMMERCE from './sample/order-ecommerce';
|
||||
import POST_METRICS_REPORT from './sample/post-metrics-report';
|
||||
import RESERVATION_REMINDER from './sample/reservation-reminder';
|
||||
import RESET_PASSWORD from './sample/reset-password';
|
||||
import RESPOND_TO_MESSAGE from './sample/respond-to-message';
|
||||
import SUBSCRIPTION_RECEIPT from './sample/subscription-receipt';
|
||||
import WELCOME from './sample/welcome';
|
||||
|
||||
export default function getConfiguration(template: string) {
|
||||
if (template.startsWith('#sample/')) {
|
||||
const sampleName = template.replace('#sample/', '');
|
||||
switch (sampleName) {
|
||||
case 'welcome':
|
||||
return WELCOME;
|
||||
case 'one-time-password':
|
||||
return ONE_TIME_PASSCODE;
|
||||
case 'order-ecomerce':
|
||||
return ORDER_ECOMMERCE;
|
||||
case 'post-metrics-report':
|
||||
return POST_METRICS_REPORT;
|
||||
case 'reservation-reminder':
|
||||
return RESERVATION_REMINDER;
|
||||
case 'reset-password':
|
||||
return RESET_PASSWORD;
|
||||
case 'respond-to-message':
|
||||
return RESPOND_TO_MESSAGE;
|
||||
case 'subscription-receipt':
|
||||
return SUBSCRIPTION_RECEIPT;
|
||||
}
|
||||
}
|
||||
|
||||
if (template.startsWith('#code/')) {
|
||||
const encodedString = template.replace('#code/', '');
|
||||
const configurationString = decodeURIComponent(atob(encodedString));
|
||||
try {
|
||||
return JSON.parse(configurationString);
|
||||
} catch {
|
||||
console.error(`Couldn't load configuration from hash.`);
|
||||
}
|
||||
}
|
||||
|
||||
return EMPTY_EMAIL_MESSAGE;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const EMPTY_EMAIL_MESSAGE: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#F5F5F5',
|
||||
canvasColor: '#FFFFFF',
|
||||
textColor: '#262626',
|
||||
fontFamily: 'MODERN_SANS',
|
||||
childrenIds: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default EMPTY_EMAIL_MESSAGE;
|
130
email-builder/src/getConfiguration/sample/one-time-passcode.ts
Normal file
130
email-builder/src/getConfiguration/sample/one-time-passcode.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const ONE_TIME_PASSCODE: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#000000',
|
||||
canvasColor: '#000000',
|
||||
textColor: '#FFFFFF',
|
||||
fontFamily: 'BOOK_SERIF',
|
||||
childrenIds: [
|
||||
'block_ChPX66qUhF46uynDE8AY11',
|
||||
'block_CkNrtQgkqPt2YWLv1hr5eJ',
|
||||
'block_BFLBa3q5y8kax9KngyXP65',
|
||||
'block_4T7sDFb4rqbSyWjLGJKmov',
|
||||
'block_Rvc8ZfTjfhXjpphHquJKvP',
|
||||
],
|
||||
},
|
||||
},
|
||||
block_ChPX66qUhF46uynDE8AY11: {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
textAlign: 'center',
|
||||
},
|
||||
props: {
|
||||
height: 24,
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_jc7ZfPvdHJ6rtH1W/&.png',
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_CkNrtQgkqPt2YWLv1hr5eJ: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#ffffff',
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Here is your one-time passcode:',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_BFLBa3q5y8kax9KngyXP65: {
|
||||
type: 'Heading',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontFamily: 'MONOSPACE',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
level: 'h1',
|
||||
text: '0123456',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_4T7sDFb4rqbSyWjLGJKmov: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#868686',
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'This code will expire in 30 minutes.',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_Rvc8ZfTjfhXjpphHquJKvP: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#868686',
|
||||
backgroundColor: null,
|
||||
fontSize: 14,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Problems? Just reply to this email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default ONE_TIME_PASSCODE;
|
1445
email-builder/src/getConfiguration/sample/order-ecommerce.ts
Normal file
1445
email-builder/src/getConfiguration/sample/order-ecommerce.ts
Normal file
File diff suppressed because it is too large
Load diff
394
email-builder/src/getConfiguration/sample/post-metrics-report.ts
Normal file
394
email-builder/src/getConfiguration/sample/post-metrics-report.ts
Normal file
|
@ -0,0 +1,394 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const POST_METRICS_REPORT: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#EEEEEE',
|
||||
canvasColor: '#FFFFFF',
|
||||
textColor: '#242424',
|
||||
fontFamily: 'MODERN_SANS',
|
||||
childrenIds: [
|
||||
'block_6B5Ke1N2KdM4STQjw7eEHT',
|
||||
'block_VE1bKDbSqiYD9VtmmaYX7w',
|
||||
'block_QQqjnauXAixe2LnJXVmHwQ',
|
||||
'block_9yEYNZmmmFauyuSKi9iJA9',
|
||||
'block_AC6eRbFVSbXHVCg2zutkLu',
|
||||
'block_CYXkzjxrj6e4Sb74Kt8quM',
|
||||
'block_AUAxG2BgwA6XC8rF5xAaaP',
|
||||
'block_C1YvcFvMvzB1Fhxn3uV8zV',
|
||||
'block_FsiiokCgr9bZitHn7sx7TB',
|
||||
'block_DomD4MLJ58VcMo49vmeTH8',
|
||||
],
|
||||
},
|
||||
},
|
||||
block_6sP1Pi9AimAoti2ZPdNXUf: {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
textAlign: 'left',
|
||||
},
|
||||
props: {
|
||||
height: 16,
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_n3eLjsf37dcjFaj5/Narrative.png',
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_9G37m6eNPw2MpUj6SYGoq1: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
borderColor: null,
|
||||
borderRadius: null,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: ['block_6sP1Pi9AimAoti2ZPdNXUf'],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_A8GU16mV1RdP85jaszN7oj: {
|
||||
type: 'Avatar',
|
||||
data: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
size: 32,
|
||||
shape: 'circle',
|
||||
imageUrl: 'https://ui-avatars.com/api/?name=John+Doe',
|
||||
alt: 'Jordan',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_4WmdbYU15yfdpYcYjsVDBA: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
borderColor: null,
|
||||
borderRadius: null,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: ['block_A8GU16mV1RdP85jaszN7oj'],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_JQAdLSAtvmfsRih13srJ8m: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
borderColor: null,
|
||||
borderRadius: null,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_6B5Ke1N2KdM4STQjw7eEHT: {
|
||||
type: 'ColumnsContainer',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
columnsCount: 2,
|
||||
columns: [
|
||||
{
|
||||
childrenIds: ['block_9G37m6eNPw2MpUj6SYGoq1'],
|
||||
},
|
||||
{
|
||||
childrenIds: ['block_4WmdbYU15yfdpYcYjsVDBA'],
|
||||
},
|
||||
{
|
||||
childrenIds: ['block_JQAdLSAtvmfsRih13srJ8m'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_VE1bKDbSqiYD9VtmmaYX7w: {
|
||||
type: 'Heading',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 0,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
level: 'h3',
|
||||
text: 'Last week, your posts received',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_QQqjnauXAixe2LnJXVmHwQ: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontSize: 48,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 0,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: '1,511',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_9yEYNZmmmFauyuSKi9iJA9: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontSize: 14,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Post impressions',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_AC6eRbFVSbXHVCg2zutkLu: {
|
||||
type: 'Button',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
buttonBackgroundColor: '#24AF7F',
|
||||
buttonStyle: 'rounded',
|
||||
buttonTextColor: '#FFFFFF',
|
||||
fullWidth: false,
|
||||
size: 'medium',
|
||||
text: 'View your analytics →',
|
||||
url: 'https://example.usewaypoint.com/post/1234/analytics',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_CYXkzjxrj6e4Sb74Kt8quM: {
|
||||
type: 'Heading',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 8,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
level: 'h3',
|
||||
text: 'Top performing post last week',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_FpDmSnPwiVzBXUvTc4yWFh: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'So excited to now have drag and drop on Waypoint. This builds on top of our new Navigator feature that we shipped earlier this week 🚢.',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_LjuDF6uu4qWL3Ju3ng63ky: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: '#FAFAFA',
|
||||
borderColor: null,
|
||||
borderRadius: 8,
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: ['block_FpDmSnPwiVzBXUvTc4yWFh'],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_AUAxG2BgwA6XC8rF5xAaaP: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
borderColor: null,
|
||||
borderRadius: null,
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: ['block_LjuDF6uu4qWL3Ju3ng63ky'],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_C1YvcFvMvzB1Fhxn3uV8zV: {
|
||||
type: 'Button',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
buttonBackgroundColor: '#EEEEEE',
|
||||
buttonStyle: 'rounded',
|
||||
buttonTextColor: '#474849',
|
||||
fullWidth: false,
|
||||
size: 'medium',
|
||||
text: 'Show more',
|
||||
url: 'https://example.usewaypoint.com/jordanisip/posts',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_FsiiokCgr9bZitHn7sx7TB: {
|
||||
type: 'Divider',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
padding: {
|
||||
top: 40,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
lineHeight: 1,
|
||||
lineColor: '#EEEEEE',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_DomD4MLJ58VcMo49vmeTH8: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#474849',
|
||||
backgroundColor: null,
|
||||
fontSize: 12,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'center',
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Questions? Just reply to this email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default POST_METRICS_REPORT;
|
2197
email-builder/src/getConfiguration/sample/reservation-reminder.ts
Normal file
2197
email-builder/src/getConfiguration/sample/reservation-reminder.ts
Normal file
File diff suppressed because it is too large
Load diff
156
email-builder/src/getConfiguration/sample/reset-password.ts
Normal file
156
email-builder/src/getConfiguration/sample/reset-password.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const RESET_PASSWORD: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#F2F5F7',
|
||||
canvasColor: '#FFFFFF',
|
||||
textColor: '#242424',
|
||||
fontFamily: 'MODERN_SANS',
|
||||
childrenIds: [
|
||||
'block_3gpSGmkgL4nWSBQjWCjK2z',
|
||||
'block_BjpQ7DGTtvaEuYRMd7VE7w',
|
||||
'block_xyg4GWmgGbJJEDRQc76bC',
|
||||
'block_76VptLCZ47t3EkAarUufEJ',
|
||||
'block_Gtk3kDYwsJqEmQf2XGWPRc',
|
||||
'block_LACDCzUS2bsvEbmnq1KHuW',
|
||||
],
|
||||
},
|
||||
},
|
||||
block_3gpSGmkgL4nWSBQjWCjK2z: {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 8,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
backgroundColor: null,
|
||||
textAlign: 'left',
|
||||
},
|
||||
props: {
|
||||
height: 24,
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_Xh1R23U9ziyct9nd/codoc.png',
|
||||
alt: '',
|
||||
linkHref: null,
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_BjpQ7DGTtvaEuYRMd7VE7w: {
|
||||
type: 'Heading',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 32,
|
||||
bottom: 0,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
level: 'h3',
|
||||
text: 'Reset your password?',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_xyg4GWmgGbJJEDRQc76bC: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#474849',
|
||||
backgroundColor: null,
|
||||
fontSize: 14,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 8,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: `If you didn't request a reset, don't worry. You can safely ignore this email.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
block_76VptLCZ47t3EkAarUufEJ: {
|
||||
type: 'Button',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
fontSize: 14,
|
||||
fontFamily: null,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 12,
|
||||
bottom: 32,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
buttonBackgroundColor: '#0068FF',
|
||||
buttonStyle: 'rectangle',
|
||||
buttonTextColor: '#FFFFFF',
|
||||
fullWidth: false,
|
||||
size: 'medium',
|
||||
text: 'Change my password',
|
||||
url: 'https://example.usewaypoint.com/reset_password?token=02938409809w8r09a83wr098aw0',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_Gtk3kDYwsJqEmQf2XGWPRc: {
|
||||
type: 'Divider',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
lineHeight: 1,
|
||||
lineColor: '#EEEEEE',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_LACDCzUS2bsvEbmnq1KHuW: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: '#474849',
|
||||
backgroundColor: null,
|
||||
fontSize: 12,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 4,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Need help? Just reply to this email to contact support.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RESET_PASSWORD;
|
175
email-builder/src/getConfiguration/sample/respond-to-message.ts
Normal file
175
email-builder/src/getConfiguration/sample/respond-to-message.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const RESPOND_TO_MESSAGE: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#F0ECE5',
|
||||
canvasColor: '#F0ECE5',
|
||||
textColor: '#030303',
|
||||
fontFamily: 'MODERN_SERIF',
|
||||
childrenIds: [
|
||||
'block_HjX7RN8eDEz7BLBHSQCNgU',
|
||||
'block_Jf65r5cUAnEzBfxnHKGa5S',
|
||||
'block_WmPDNHDpyHkygqjHuqF7St',
|
||||
'block_4VCKUvRMo7EbuMdN1VsdRw',
|
||||
'block_4siwziT4BkewmN55PpXvEu',
|
||||
'block_S9Rg9F3bGcviRyfMpoU5e4',
|
||||
],
|
||||
},
|
||||
},
|
||||
block_HjX7RN8eDEz7BLBHSQCNgU: {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
padding: {
|
||||
top: 8,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
height: 32,
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_hW6RusynHUNTKoLm/boop.png',
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_Jf65r5cUAnEzBfxnHKGa5S: {
|
||||
type: 'Heading',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontFamily: null,
|
||||
fontWeight: null,
|
||||
textAlign: null,
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 0,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
level: 'h2',
|
||||
text: `Respond to Anna's Inquiry`,
|
||||
},
|
||||
},
|
||||
},
|
||||
block_WmPDNHDpyHkygqjHuqF7St: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: null,
|
||||
textAlign: null,
|
||||
padding: {
|
||||
top: 8,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Dog boarding for Aug 1 - Aug 29.',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_95nkowWyi4p2VBiA46Eizs: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: '#faf9f9',
|
||||
fontSize: 21,
|
||||
fontFamily: null,
|
||||
fontWeight: null,
|
||||
textAlign: null,
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Any chance you can watch Emma again next month?',
|
||||
},
|
||||
},
|
||||
},
|
||||
block_4VCKUvRMo7EbuMdN1VsdRw: {
|
||||
type: 'Container',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
borderColor: null,
|
||||
borderRadius: null,
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
childrenIds: ['block_95nkowWyi4p2VBiA46Eizs'],
|
||||
},
|
||||
},
|
||||
},
|
||||
block_4siwziT4BkewmN55PpXvEu: {
|
||||
type: 'Button',
|
||||
data: {
|
||||
style: {
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: null,
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
buttonBackgroundColor: '#BE4F46',
|
||||
buttonTextColor: '#FFFFFF',
|
||||
size: 'large',
|
||||
buttonStyle: 'pill',
|
||||
text: 'Respond',
|
||||
url: 'http://example.usewaypoint.com/request/2334234',
|
||||
fullWidth: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
block_S9Rg9F3bGcviRyfMpoU5e4: {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
color: null,
|
||||
backgroundColor: null,
|
||||
fontSize: 16,
|
||||
fontFamily: null,
|
||||
fontWeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'You need 2 more walks to become a Super Walker on Boop!',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RESPOND_TO_MESSAGE;
|
1349
email-builder/src/getConfiguration/sample/subscription-receipt.ts
Normal file
1349
email-builder/src/getConfiguration/sample/subscription-receipt.ts
Normal file
File diff suppressed because it is too large
Load diff
170
email-builder/src/getConfiguration/sample/welcome.ts
Normal file
170
email-builder/src/getConfiguration/sample/welcome.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { TEditorConfiguration } from '../../documents/editor/core';
|
||||
|
||||
const WELCOME: TEditorConfiguration = {
|
||||
root: {
|
||||
type: 'EmailLayout',
|
||||
data: {
|
||||
backdropColor: '#F2F5F7',
|
||||
canvasColor: '#FFFFFF',
|
||||
textColor: '#242424',
|
||||
fontFamily: 'MODERN_SANS',
|
||||
childrenIds: [
|
||||
'block-1709571212684',
|
||||
'block-1709571228545',
|
||||
'block-1709571234315',
|
||||
'block-1709571247550',
|
||||
'block-1709571258507',
|
||||
'block-1709571281151',
|
||||
'block-1709571302968',
|
||||
'block-1709571282795',
|
||||
],
|
||||
},
|
||||
},
|
||||
'block-1709571212684': {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
padding: {
|
||||
top: 24,
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_JTNBBPGrNs2Ph4JL/marketbase.png',
|
||||
alt: 'Marketbase',
|
||||
linkHref: 'https://marketbase.app',
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571228545': {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
fontWeight: 'normal',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Hi Anna 👋,',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571234315': {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
fontWeight: 'normal',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Welcome to Marketbase! Marketbase is how teams within fast growing marketplaces effortlessly monitor conversations to prevent disintermediation, identify problematic users, and increase trust & safety within their community.',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571247550': {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
fontWeight: 'normal',
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'Best of all, you can connect your existing messaging services in minutes:',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571258507': {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_oWB821TUkDXvr2f4/Screenshot%202023-11-22%20at%2011.51.30%20AM.png',
|
||||
alt: 'Video thumbnail',
|
||||
linkHref: 'https://capture.dropbox.com/NBQEmoCKKP9RGBWr',
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571281151': {
|
||||
type: 'Text',
|
||||
data: {
|
||||
style: {
|
||||
fontWeight: 'normal',
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 16,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
text: 'If you ever need help, just reply to this email and one of us will get back to you shortly. We’re here to help.',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571282795': {
|
||||
type: 'Image',
|
||||
data: {
|
||||
style: {
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 40,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_cAK8FpmBEGoSRNi3/Screenshot%202023-11-22%20at%2011.48.53%20AM.png',
|
||||
alt: 'Illustration',
|
||||
linkHref: null,
|
||||
contentAlignment: 'middle',
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-1709571302968': {
|
||||
type: 'Button',
|
||||
data: {
|
||||
style: {
|
||||
fontSize: 14,
|
||||
padding: {
|
||||
top: 16,
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
left: 24,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
buttonBackgroundColor: '#0079cc',
|
||||
buttonStyle: 'rectangle',
|
||||
text: 'Open dashboard',
|
||||
url: 'https://www.usewaypoint.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default WELCOME;
|
34
email-builder/src/main.tsx
Normal file
34
email-builder/src/main.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App, { AppProps, DEFAULT_SOURCE } from './App';
|
||||
import { setDocument, resetDocument } from './documents/editor/EditorContext';
|
||||
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import theme from './theme';
|
||||
|
||||
function isRendered(containerId: string): boolean {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error(`Container with id ${containerId} not found`);
|
||||
return false;
|
||||
}
|
||||
return container.hasChildNodes();
|
||||
}
|
||||
|
||||
function render(containerId: string, props: AppProps, force: boolean = false) {
|
||||
if (!isRendered(containerId) || force) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
ReactDOM.createRoot(container).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App {...props} />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { App, setDocument, resetDocument, render, isRendered, DEFAULT_SOURCE };
|
520
email-builder/src/theme.ts
Normal file
520
email-builder/src/theme.ts
Normal file
|
@ -0,0 +1,520 @@
|
|||
import { alpha, createTheme, darken, lighten } from '@mui/material/styles';
|
||||
|
||||
const BRAND_NAVY = '#212443';
|
||||
const BRAND_BLUE = '#0079CC';
|
||||
const BRAND_GREEN = '#1F8466';
|
||||
const BRAND_RED = '#E81212';
|
||||
const BRAND_YELLOW = '#F6DC9F';
|
||||
const BRAND_PURPLE = '#6C0E7C';
|
||||
const BRAND_BROWN = '#CC996C';
|
||||
const STANDARD_FONT_FAMILY =
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
|
||||
const MONOSPACE_FONT_FAMILY =
|
||||
'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace';
|
||||
|
||||
const BASE_THEME = createTheme({
|
||||
palette: {
|
||||
background: {
|
||||
default: '#f2f5f7',
|
||||
},
|
||||
text: {
|
||||
primary: '#1F1F21',
|
||||
secondary: '#4F4F4F',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: STANDARD_FONT_FAMILY,
|
||||
},
|
||||
});
|
||||
|
||||
const THEME = createTheme(BASE_THEME, {
|
||||
palette: {
|
||||
brand: {
|
||||
navy: BRAND_NAVY,
|
||||
blue: BRAND_BLUE,
|
||||
red: BRAND_RED,
|
||||
green: BRAND_GREEN,
|
||||
yellow: BRAND_YELLOW,
|
||||
purple: BRAND_PURPLE,
|
||||
brown: BRAND_BROWN,
|
||||
},
|
||||
success: {
|
||||
main: BRAND_GREEN,
|
||||
light: lighten(BRAND_GREEN, 0.15),
|
||||
dark: darken(BRAND_GREEN, 0.15),
|
||||
},
|
||||
error: {
|
||||
main: BRAND_RED,
|
||||
light: lighten(BRAND_RED, 0.15),
|
||||
dark: darken(BRAND_RED, 0.15),
|
||||
},
|
||||
cadet: {
|
||||
100: '#F9FAFB',
|
||||
200: '#F2F5F7',
|
||||
300: '#DCE4EA',
|
||||
400: '#A8BBCA',
|
||||
500: '#6A8BA4',
|
||||
},
|
||||
highlight: {
|
||||
100: lighten(BRAND_YELLOW, 0.8),
|
||||
200: lighten(BRAND_YELLOW, 0.6),
|
||||
300: lighten(BRAND_YELLOW, 0.4),
|
||||
400: lighten(BRAND_YELLOW, 0.2),
|
||||
500: BRAND_YELLOW,
|
||||
},
|
||||
info: {
|
||||
main: BRAND_BLUE,
|
||||
},
|
||||
primary: {
|
||||
main: BRAND_BLUE,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: `
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
pre {
|
||||
font-family: ${MONOSPACE_FONT_FAMILY}
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
`,
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
},
|
||||
action: {
|
||||
paddingTop: 0,
|
||||
marginRight: 0,
|
||||
},
|
||||
filledSuccess: {
|
||||
backgroundColor: BRAND_GREEN,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiStepLabel: {
|
||||
styleOverrides: {
|
||||
label: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
defaultProps: {
|
||||
fullWidth: true,
|
||||
},
|
||||
},
|
||||
MuiDialogContent: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingTop: BASE_THEME.spacing(1),
|
||||
paddingBottom: BASE_THEME.spacing(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialogTitle: {
|
||||
defaultProps: {
|
||||
variant: 'h4',
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingTop: BASE_THEME.spacing(3),
|
||||
paddingBottom: BASE_THEME.spacing(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialogActions: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderTop: '1px solid',
|
||||
borderTopColor: BASE_THEME.palette.divider,
|
||||
marginTop: BASE_THEME.spacing(2.5),
|
||||
padding: `${BASE_THEME.spacing(1.5)} ${BASE_THEME.spacing(3)}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
...BASE_THEME.typography.body2,
|
||||
borderColor: BASE_THEME.palette.grey[200],
|
||||
},
|
||||
head: {
|
||||
...BASE_THEME.typography.overline,
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
letterSpacing: '0.075em',
|
||||
color: BASE_THEME.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:last-child td': {
|
||||
borderBottom: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAvatar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'uppercase',
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.MuiChip-filledError, &.MuiChip-filledSuccess': {
|
||||
fill: BASE_THEME.palette.primary.contrastText,
|
||||
},
|
||||
},
|
||||
sizeSmall: {
|
||||
borderRadius: BASE_THEME.spacing(0.5),
|
||||
fontSize: 12,
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 14,
|
||||
marginLeft: BASE_THEME.spacing(1),
|
||||
},
|
||||
colorSecondary: {
|
||||
borderColor: BASE_THEME.palette.grey[400],
|
||||
color: BASE_THEME.palette.text.secondary,
|
||||
},
|
||||
label: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDrawer: {
|
||||
defaultProps: {
|
||||
PaperProps: {
|
||||
elevation: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(12),
|
||||
backgroundColor: alpha(BASE_THEME.palette.text.primary, 0.9),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSlider: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
height: 1,
|
||||
},
|
||||
track: {
|
||||
height: 1,
|
||||
border: 'none',
|
||||
},
|
||||
rail: {
|
||||
height: 1,
|
||||
backgroundColor: BASE_THEME.palette.grey[500],
|
||||
},
|
||||
mark: {
|
||||
backgroundColor: BASE_THEME.palette.grey[500],
|
||||
},
|
||||
markActive: {
|
||||
height: 0,
|
||||
},
|
||||
thumb: {
|
||||
height: 16,
|
||||
width: 16,
|
||||
cursor: 'col-resize',
|
||||
'&:hover, &.Mui-active, &.Mui-focusVisible': {
|
||||
boxShadow: `0 0 0 4px ${alpha(BRAND_BLUE, 0.2)}`,
|
||||
},
|
||||
'&:before': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
elevation: 2,
|
||||
square: true,
|
||||
},
|
||||
},
|
||||
MuiButtonBase: {
|
||||
defaultProps: {
|
||||
disableTouchRipple: true,
|
||||
focusRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.MuiButton-containedSecondary.Mui-disabled': {
|
||||
backgroundColor: BASE_THEME.palette.grey[100],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButtonGroup: {
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
edgeStart: {
|
||||
marginLeft: BASE_THEME.spacing(-1),
|
||||
},
|
||||
colorSecondary: {
|
||||
color: BASE_THEME.palette.grey[500],
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
textPrimary: {
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
textSecondary: {
|
||||
color: BASE_THEME.palette.text.secondary,
|
||||
},
|
||||
outlinedPrimary: {
|
||||
borderColor: BASE_THEME.palette.grey[300],
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
'&:hover, &:active, &:focus': {
|
||||
borderColor: BASE_THEME.palette.grey[500],
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
},
|
||||
containedSecondary: {
|
||||
backgroundColor: BASE_THEME.palette.common.white,
|
||||
border: `1px solid ${BASE_THEME.palette.grey[300]}`,
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
'&:hover, &:active, &:focus': {
|
||||
backgroundColor: BASE_THEME.palette.common.white,
|
||||
borderColor: BASE_THEME.palette.grey[500],
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiToggleButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
paddingLeft: BASE_THEME.spacing(1.5),
|
||||
paddingRight: BASE_THEME.spacing(1.5),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&:not(.Mui-disabled, .Mui-error):before': {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.grey[400]}`,
|
||||
},
|
||||
'&:hover:not(.Mui-disabled, .Mui-error):before': {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.grey[500]} !important`,
|
||||
},
|
||||
'&:after': {
|
||||
borderBottom: `1px solid ${BASE_THEME.palette.text.primary} !important`,
|
||||
},
|
||||
'&.MuiOutlinedInput-root:not(.Mui-error)': {
|
||||
'& fieldset': {
|
||||
borderColor: BASE_THEME.palette.grey[300],
|
||||
transition: 'border-color 0.2s',
|
||||
},
|
||||
},
|
||||
'&.MuiOutlinedInput-root:not(.Mui-disabled, .Mui-error)': {
|
||||
'&:hover fieldset': {
|
||||
borderColor: BASE_THEME.palette.grey[400],
|
||||
},
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: BASE_THEME.palette.text.secondary,
|
||||
borderWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
'&.Mui-disabled': {
|
||||
WebkitTextFillColor: 'inherit',
|
||||
color: BASE_THEME.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
inputSizeSmall: {},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
notchedOutline: {
|
||||
'& legend': {
|
||||
fontSize: '0.85em',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputAdornment: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiTypography-root': {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
color: BASE_THEME.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputLabel: {
|
||||
defaultProps: {
|
||||
shrink: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
shrink: {
|
||||
transform: 'scale(0.85)',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
'&.Mui-focused': {
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
'&.MuiInputLabel-standard': {
|
||||
transform: 'translate(0, -4px) scale(0.85)',
|
||||
color: '#4F4F4F',
|
||||
},
|
||||
'&.MuiInputLabel-outlined': {
|
||||
transform: 'translate(15px, -8px) scale(0.85)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
defaultProps: {
|
||||
variant: 'scrollable',
|
||||
},
|
||||
styleOverrides: {
|
||||
indicator: {
|
||||
height: 1,
|
||||
backgroundColor: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
minWidth: BASE_THEME.spacing(2),
|
||||
paddingLeft: BASE_THEME.spacing(1.5),
|
||||
paddingRight: BASE_THEME.spacing(1.5),
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
lineHeight: 1.5,
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
transition: 'color 0.2s',
|
||||
'&.Mui-selected': {
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
'&:hover': {
|
||||
color: BASE_THEME.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardHeader: {
|
||||
styleOverrides: {
|
||||
title: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
h1: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(40),
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '-0.02em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
h2: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(32),
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '-0.02em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(24),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '-0.01em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
h4: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(20),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '-0.01em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
h5: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(18),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '-0.01em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
h6: {
|
||||
fontFamily: BASE_THEME.typography.fontFamily,
|
||||
fontSize: BASE_THEME.typography.pxToRem(16),
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: '-0.005em',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
},
|
||||
body1: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(14),
|
||||
},
|
||||
body2: {
|
||||
fontSize: BASE_THEME.typography.pxToRem(12),
|
||||
},
|
||||
overline: {
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
letterSpacing: '0.05em',
|
||||
},
|
||||
button: {
|
||||
textTransform: 'none',
|
||||
fontWeight: BASE_THEME.typography.fontWeightMedium,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
caption: {
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
},
|
||||
shadows: [
|
||||
'none',
|
||||
'0px 4px 15px rgba(33, 36, 67, 0.04), 0px 0px 2px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)',
|
||||
'0px 10px 20px rgba(33, 36, 67, 0.04), 0px 2px 6px rgba(33, 36, 67, 0.04), 0px 0px 1px rgba(33, 36, 67, 0.04)',
|
||||
'0px 16px 24px rgba(33, 36, 67, 0.05), 0px 2px 6px rgba(33, 36, 67, 0.05), 0px 0px 1px rgba(33, 36, 67, 0.05)',
|
||||
'0px 24px 32px rgba(33, 36, 67, 0.06), 0px 16px 24px rgba(33, 36, 67, 0.06), 0px 4px 8px rgba(33, 36, 67, 0.06)',
|
||||
...Array(20).fill('none'),
|
||||
],
|
||||
});
|
||||
|
||||
export default THEME;
|
1
email-builder/src/vite-env.d.ts
vendored
Normal file
1
email-builder/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
22
email-builder/tsconfig.json
Normal file
22
email-builder/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"lib": [],
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declarationMap": true,
|
||||
"declaration": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["dist"]
|
||||
}
|
31
email-builder/vite.config.ts
Normal file
31
email-builder/vite.config.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.tsx'),
|
||||
name: 'EmailBuilder',
|
||||
fileName: (format) => `email-builder.${format}.js`,
|
||||
},
|
||||
minify: 'terser',
|
||||
cssCodeSplit: true,
|
||||
cssMinify: true,
|
||||
|
||||
// Option to externalize deps.
|
||||
// rollupOptions: {
|
||||
// external: ['react', 'react-dom'],
|
||||
// output: {
|
||||
// globals: {
|
||||
// react: 'React',
|
||||
// 'react-dom': 'ReactDOM',
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
}
|
||||
});
|
|
@ -5,8 +5,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { render, isRendered, setDocument, DEFAULT_SOURCE } from '../email-builder';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
source: { type: String, default: '' },
|
||||
|
@ -20,7 +18,7 @@ export default {
|
|||
source = JSON.parse(this.$props.source);
|
||||
}
|
||||
|
||||
render('visual-editor', {
|
||||
window.EmailBuilder.render('visual-editor', {
|
||||
data: source,
|
||||
onChange: (data, body) => {
|
||||
this.$emit('change', { source: JSON.stringify(data), body });
|
||||
|
@ -28,19 +26,42 @@ export default {
|
|||
height: this.height,
|
||||
});
|
||||
},
|
||||
|
||||
loadScript() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.EmailBuilder) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.id = 'email-builder-script';
|
||||
script.src = '/admin/static/email-builder/email-builder.umd.js';
|
||||
script.onload = () => {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initEditor();
|
||||
this.loadScript().then(() => {
|
||||
this.initEditor();
|
||||
}).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to load email-builder script:', error);
|
||||
});
|
||||
},
|
||||
|
||||
watch: {
|
||||
source(val) {
|
||||
if (isRendered('visual-editor')) {
|
||||
if (window.EmailBuilder?.isRendered('visual-editor')) {
|
||||
if (val) {
|
||||
setDocument(JSON.parse(val));
|
||||
window.EmailBuilder.setDocument(JSON.parse(val));
|
||||
} else {
|
||||
setDocument(DEFAULT_SOURCE);
|
||||
window.EmailBuilder.setDocument(window.EmailBuilder.DEFAULT_SOURCE);
|
||||
}
|
||||
} else {
|
||||
this.initEditor();
|
||||
|
|
|
@ -327,7 +327,12 @@ export default Vue.extend({
|
|||
lists: [],
|
||||
tags: [],
|
||||
sendAt: null,
|
||||
content: { contentType: 'richtext', body: '', bodySource: null, templateId: null },
|
||||
content: {
|
||||
contentType: 'richtext',
|
||||
body: '',
|
||||
bodySource: null,
|
||||
templateId: null,
|
||||
},
|
||||
altbody: null,
|
||||
media: [],
|
||||
|
||||
|
@ -447,7 +452,12 @@ export default Vue.extend({
|
|||
archiveMetaStr: data.archiveMeta ? JSON.stringify(data.archiveMeta, null, 4) : '{}',
|
||||
|
||||
// The structure that is populated by editor input event.
|
||||
content: { contentType: data.contentType, body: data.body, bodySource: data.bodySource, templateId: data.templateId },
|
||||
content: {
|
||||
contentType: data.contentType,
|
||||
body: data.body,
|
||||
bodySource: data.bodySource,
|
||||
templateId: data.templateId,
|
||||
},
|
||||
};
|
||||
this.isAttachFieldVisible = this.form.media.length > 0;
|
||||
|
||||
|
|
184
frontend/yarn.lock
vendored
184
frontend/yarn.lock
vendored
|
@ -230,18 +230,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
|
||||
dependencies:
|
||||
string-width "^5.1.2"
|
||||
string-width-cjs "npm:string-width@^4.2.0"
|
||||
strip-ansi "^7.0.1"
|
||||
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
|
||||
wrap-ansi "^8.1.0"
|
||||
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
|
||||
|
||||
"@kurkle/color@^0.3.0":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf"
|
||||
|
@ -278,16 +266,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
|
||||
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.22.4":
|
||||
version "4.22.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5"
|
||||
integrity sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==
|
||||
|
||||
"@parcel/watcher-android-arm64@2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a"
|
||||
|
@ -616,11 +594,6 @@ ansi-regex@^5.0.1:
|
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-regex@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
|
||||
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
|
||||
|
||||
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
|
@ -628,19 +601,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
|||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^6.1.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
|
||||
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arch@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
||||
|
@ -864,7 +824,7 @@ brace-expansion@^2.0.1:
|
|||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@~3.0.2:
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
|
@ -1273,11 +1233,6 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
|||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
|
@ -1983,18 +1938,6 @@ glob@^10.3.3:
|
|||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^10.3.3:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^3.1.2"
|
||||
minimatch "^9.0.4"
|
||||
minipass "^7.1.2"
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
|
@ -2139,6 +2082,11 @@ indent-string@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
|
||||
|
||||
indent.js@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/indent.js/-/indent.js-0.3.5.tgz#e2fefa04043b7be69c33635dc9e0cfb9cc7c4d7f"
|
||||
integrity sha512-wiTA5fEz0kc8tHzY6CSujl/k62WVNvTxAZzmPe5V7MYxRCeGGibPCIYWYBzPp/bcJh3CXUO/8qrTrO/x9s1i2Q==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
|
@ -2162,15 +2110,6 @@ ini@^1.3.4:
|
|||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
internal-slot@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986"
|
||||
integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==
|
||||
dependencies:
|
||||
get-intrinsic "^1.2.0"
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
internal-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
||||
|
@ -2448,31 +2387,6 @@ js-cookie@^3.0.5:
|
|||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
|
||||
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||
|
||||
jackspeak@^3.1.2:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
||||
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^8.0.2"
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
js-beautify@^1.15.1:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.1.tgz#4695afb508c324e1084ee0b952a102023fc65b64"
|
||||
integrity sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==
|
||||
dependencies:
|
||||
config-chain "^1.1.13"
|
||||
editorconfig "^1.0.4"
|
||||
glob "^10.3.3"
|
||||
js-cookie "^3.0.5"
|
||||
nopt "^7.2.0"
|
||||
|
||||
js-cookie@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
|
||||
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
@ -2649,12 +2563,10 @@ lru-cache@^10.2.0:
|
|||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -2717,11 +2629,6 @@ minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8:
|
|||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@^2.1.1, ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
|
@ -2737,6 +2644,11 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
node-addon-api@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
nopt@^7.2.0:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7"
|
||||
|
@ -2744,11 +2656,6 @@ nopt@^7.2.0:
|
|||
dependencies:
|
||||
abbrev "^2.0.0"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
npm-run-path@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
|
@ -3378,15 +3285,6 @@ sshpk@^1.18.0:
|
|||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
|
@ -3396,19 +3294,10 @@ string-width@^4.1.0, string-width@^4.2.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
|
||||
dependencies:
|
||||
eastasianwidth "^0.2.0"
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string.prototype.matchall@^4.0.8:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100"
|
||||
integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==
|
||||
string.prototype.includes@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92"
|
||||
integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==
|
||||
dependencies:
|
||||
call-bind "^1.0.7"
|
||||
define-properties "^1.2.1"
|
||||
|
@ -3473,13 +3362,6 @@ string.prototype.trimstart@^1.0.8:
|
|||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
|
@ -3487,13 +3369,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-bom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||
|
@ -3850,14 +3725,10 @@ which@^2.0.1:
|
|||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
word-wrap@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
|
@ -3877,15 +3748,6 @@ wrap-ansi@^7.0.0:
|
|||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
|
||||
dependencies:
|
||||
ansi-styles "^6.1.0"
|
||||
string-width "^5.0.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
|
Loading…
Add table
Reference in a new issue