├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── ci.yaml
│ └── github-pages.yaml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── images
├── builder.png
├── designer.png
├── mobile.png
├── output.png
└── template.png
├── jest.config.ts
├── package-lock.json
├── package.json
├── packages
├── block-avatar
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-button
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-columns-container
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-container
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-divider
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-heading
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-html
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-image
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-spacer
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── block-text
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── EmailMarkdown.tsx
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.spec.tsx
│ │ └── index.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── document-core
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── builders
│ │ │ ├── buildBlockComponent.tsx
│ │ │ ├── buildBlockConfigurationDictionary.ts
│ │ │ └── buildBlockConfigurationSchema.ts
│ │ ├── index.ts
│ │ └── utils.ts
│ ├── tests
│ │ └── builder
│ │ │ ├── __snapshots__
│ │ │ └── buildBlockComponent.spec.tsx.snap
│ │ │ ├── buildBlockComponent.spec.tsx
│ │ │ └── buildBlockConfigurationSchema.spec.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
├── editor-sample
│ ├── LICENSE
│ ├── README.md
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── App
│ │ │ ├── InspectorDrawer
│ │ │ │ ├── ConfigurationPanel
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── input-panels
│ │ │ │ │ │ ├── AvatarSidebarPanel.tsx
│ │ │ │ │ │ ├── ButtonSidebarPanel.tsx
│ │ │ │ │ │ ├── ColumnsContainerSidebarPanel.tsx
│ │ │ │ │ │ ├── ContainerSidebarPanel.tsx
│ │ │ │ │ │ ├── DividerSidebarPanel.tsx
│ │ │ │ │ │ ├── EmailLayoutSidebarPanel.tsx
│ │ │ │ │ │ ├── HeadingSidebarPanel.tsx
│ │ │ │ │ │ ├── HtmlSidebarPanel.tsx
│ │ │ │ │ │ ├── ImageSidebarPanel.tsx
│ │ │ │ │ │ ├── SpacerSidebarPanel.tsx
│ │ │ │ │ │ ├── TextSidebarPanel.tsx
│ │ │ │ │ │ └── helpers
│ │ │ │ │ │ ├── BaseSidebarPanel.tsx
│ │ │ │ │ │ ├── inputs
│ │ │ │ │ │ ├── BooleanInput.tsx
│ │ │ │ │ │ ├── ColorInput
│ │ │ │ │ │ │ ├── BaseColorInput.tsx
│ │ │ │ │ │ │ ├── Picker.tsx
│ │ │ │ │ │ │ ├── Swatch.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── ColumnWidthsInput.tsx
│ │ │ │ │ │ ├── FontFamily.tsx
│ │ │ │ │ │ ├── FontSizeInput.tsx
│ │ │ │ │ │ ├── FontWeightInput.tsx
│ │ │ │ │ │ ├── PaddingInput.tsx
│ │ │ │ │ │ ├── RadioGroupInput.tsx
│ │ │ │ │ │ ├── SliderInput.tsx
│ │ │ │ │ │ ├── TextAlignInput.tsx
│ │ │ │ │ │ ├── TextDimensionInput.tsx
│ │ │ │ │ │ ├── TextInput.tsx
│ │ │ │ │ │ └── raw
│ │ │ │ │ │ │ └── RawSliderInput.tsx
│ │ │ │ │ │ └── style-inputs
│ │ │ │ │ │ ├── MultiStylePropertyPanel.tsx
│ │ │ │ │ │ └── SingleStylePropertyPanel.tsx
│ │ │ │ ├── StylesPanel.tsx
│ │ │ │ ├── ToggleInspectorPanelButton.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── SamplesDrawer
│ │ │ │ ├── SidebarButton.tsx
│ │ │ │ ├── ToggleSamplesPanelButton.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── waypoint.svg
│ │ │ ├── TemplatePanel
│ │ │ │ ├── DownloadJson
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── HtmlPanel.tsx
│ │ │ │ ├── ImportJson
│ │ │ │ │ ├── ImportJsonDialog.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── validateJsonStringValue.ts
│ │ │ │ ├── JsonPanel.tsx
│ │ │ │ ├── MainTabsGroup.tsx
│ │ │ │ ├── ShareButton.tsx
│ │ │ │ ├── helper
│ │ │ │ │ ├── HighlightedCodePanel.tsx
│ │ │ │ │ └── highlighters.tsx
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── documents
│ │ │ ├── blocks
│ │ │ │ ├── ColumnsContainer
│ │ │ │ │ ├── ColumnsContainerEditor.tsx
│ │ │ │ │ └── ColumnsContainerPropsSchema.ts
│ │ │ │ ├── Container
│ │ │ │ │ ├── ContainerEditor.tsx
│ │ │ │ │ └── ContainerPropsSchema.tsx
│ │ │ │ ├── EmailLayout
│ │ │ │ │ ├── EmailLayoutEditor.tsx
│ │ │ │ │ └── EmailLayoutPropsSchema.tsx
│ │ │ │ └── helpers
│ │ │ │ │ ├── EditorChildrenIds
│ │ │ │ │ ├── AddBlockMenu
│ │ │ │ │ │ ├── BlockButton.tsx
│ │ │ │ │ │ ├── BlocksMenu.tsx
│ │ │ │ │ │ ├── DividerButton.tsx
│ │ │ │ │ │ ├── PlaceholderButton.tsx
│ │ │ │ │ │ ├── buttons.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── TStyle.ts
│ │ │ │ │ ├── block-wrappers
│ │ │ │ │ ├── EditorBlockWrapper.tsx
│ │ │ │ │ ├── ReaderBlockWrapper.tsx
│ │ │ │ │ └── TuneMenu.tsx
│ │ │ │ │ ├── fontFamily.ts
│ │ │ │ │ └── zod.ts
│ │ │ └── editor
│ │ │ │ ├── EditorBlock.tsx
│ │ │ │ ├── EditorContext.tsx
│ │ │ │ └── core.tsx
│ │ ├── favicon
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ └── favicon.ico
│ │ ├── getConfiguration
│ │ │ ├── index.tsx
│ │ │ └── sample
│ │ │ │ ├── empty-email-message.ts
│ │ │ │ ├── one-time-passcode.ts
│ │ │ │ ├── order-ecommerce.ts
│ │ │ │ ├── post-metrics-report.ts
│ │ │ │ ├── reservation-reminder.ts
│ │ │ │ ├── reset-password.ts
│ │ │ │ ├── respond-to-message.ts
│ │ │ │ ├── subscription-receipt.ts
│ │ │ │ └── welcome.ts
│ │ ├── main.tsx
│ │ ├── theme.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
└── email-builder
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── Reader
│ │ └── core.tsx
│ ├── blocks
│ │ ├── ColumnsContainer
│ │ │ ├── ColumnsContainerPropsSchema.ts
│ │ │ └── ColumnsContainerReader.tsx
│ │ ├── Container
│ │ │ ├── ContainerPropsSchema.tsx
│ │ │ └── ContainerReader.tsx
│ │ └── EmailLayout
│ │ │ ├── EmailLayoutPropsSchema.tsx
│ │ │ └── EmailLayoutReader.tsx
│ ├── index.ts
│ └── renderers
│ │ ├── renderToStaticMarkup.spec.tsx
│ │ └── renderToStaticMarkup.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
4 | "parser": "@typescript-eslint/parser",
5 | "parserOptions": {
6 | "ecmaVersion": 2015,
7 | "project": "./tsconfig.json",
8 | "sourceType": "module"
9 | },
10 | "env": {
11 | "browser": true,
12 | "node": true
13 | },
14 | "plugins": ["@typescript-eslint", "simple-import-sort"],
15 | "rules": {
16 | "@typescript-eslint/no-empty-interface": ["off"],
17 | "@typescript-eslint/no-explicit-any": ["error"],
18 | "@typescript-eslint/no-unused-vars": ["error"],
19 | "indent": ["off"],
20 | "no-empty": ["error", { "allowEmptyCatch": true }],
21 | "no-extra-boolean-cast": ["error", { "enforceForLogicalOperands": false }],
22 | "no-implicit-coercion": "error",
23 | "no-duplicate-imports": "error",
24 | "quotes": ["error", "single", { "avoidEscape": false, "allowTemplateLiterals": true }],
25 | "semi": ["error", "always"],
26 | "simple-import-sort/imports": [
27 | "error",
28 | {
29 | "groups": [["^\\u0000"], ["^\\w"], ["^@"], ["^"], ["^\\.\\."], ["^\\."]]
30 | }
31 | ]
32 | },
33 | "overrides": [
34 | {
35 | "files": ["*.spec.ts", "*.spec.tsx"],
36 | "rules": {
37 | "@typescript-eslint/no-explicit-any": ["off"]
38 | },
39 | "env": {
40 | "jest": true
41 | }
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI - Code styles, unit tests
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches:
7 | - main
8 | concurrency:
9 | group: ${{ github.head_ref }}-codestyles
10 | cancel-in-progress: true
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | timeout-minutes: 5
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 20
20 | cache: 'npm'
21 | cache-dependency-path: '**/package-lock.json'
22 | - run: |
23 | npm ci
24 | (cd ./packages/block-avatar;pwd;npm ci)
25 | (cd ./packages/block-button;pwd;npm ci)
26 | (cd ./packages/block-divider;pwd;npm ci)
27 | (cd ./packages/block-heading;pwd;npm ci)
28 | (cd ./packages/block-html;pwd;npm ci)
29 | (cd ./packages/block-image;pwd;npm ci)
30 | (cd ./packages/block-spacer;pwd;npm ci)
31 | (cd ./packages/block-text;pwd;npm ci)
32 | (cd ./packages/document-core;pwd;npm ci)
33 | (cd ./packages/editor-sample;pwd;npm ci)
34 | (cd ./packages/email-builder;pwd;npm ci)
35 | - run: npx eslint .
36 | - run: npx prettier . --check
37 | - run: npm test
38 | - run: ./node_modules/.bin/tsc --noEmit
39 |
--------------------------------------------------------------------------------
/.github/workflows/github-pages.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy static content to Pages
2 | on:
3 | push:
4 | branches: ['main']
5 | workflow_dispatch:
6 | permissions:
7 | contents: read
8 | pages: write
9 | id-token: write
10 | concurrency:
11 | group: 'pages'
12 | cancel-in-progress: true
13 | jobs:
14 | deploy:
15 | environment:
16 | name: github-pages
17 | url: ${{ steps.deployment.outputs.page_url }}
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | - name: Set up Node
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 20
26 | cache: 'npm'
27 | - name: Install dependencies and build
28 | working-directory: './packages/editor-sample'
29 | run: |
30 | npm ci
31 | npm run build
32 | - name: Setup Pages
33 | uses: actions/configure-pages@v3
34 | - name: Upload artifact
35 | uses: actions/upload-pages-artifact@v2
36 | with:
37 | path: './packages/editor-sample/dist'
38 | - name: Deploy to GitHub Pages
39 | id: deployment
40 | uses: actions/deploy-pages@v2
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # misc
5 | .esbuild
6 | .DS_Store
7 | .env
8 | .envrc
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | dist
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "printWidth": 120,
5 | "proseWrap": "preserve",
6 | "semi": true,
7 | "singleQuote": true,
8 | "tabWidth": 2,
9 | "trailingComma": "es5",
10 | "useTabs": false
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/images/builder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/images/builder.png
--------------------------------------------------------------------------------
/images/designer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/images/designer.png
--------------------------------------------------------------------------------
/images/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/images/mobile.png
--------------------------------------------------------------------------------
/images/output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/images/output.png
--------------------------------------------------------------------------------
/images/template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/images/template.png
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | preset: 'ts-jest',
5 | testEnvironment: 'jsdom',
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint-monorepo",
3 | "version": "0.0.6",
4 | "main": "dist/index.js",
5 | "types": "dist/index.d.ts",
6 | "target": "ES2022",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "build": "npx tsc",
12 | "test": "npx jest"
13 | },
14 | "author": "carlos@usewaypoint.com",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@jest/globals": "^29.7.0",
18 | "@testing-library/react": "^14.2.1",
19 | "@types/jest": "^29.5.12",
20 | "@typescript-eslint/eslint-plugin": "^6.21.0",
21 | "eslint": "^8.56.0",
22 | "eslint-plugin-react-hooks": "^4.6.0",
23 | "eslint-plugin-simple-import-sort": "^11.0.0",
24 | "jest": "^29.7.0",
25 | "jest-environment-jsdom": "^29.7.0",
26 | "prettier": "^3.2.5",
27 | "react-test-renderer": "^18.2.0",
28 | "ts-jest": "^29.1.2",
29 | "ts-node": "^10.9.2",
30 | "tsup": "^8.0.2",
31 | "typescript": "^5.3.3",
32 | "zod": "^3.22.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/block-avatar/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-avatar/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-avatar/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-avatar
2 |
3 | Avatar component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-avatar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-avatar",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible Avatar component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-avatar/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-avatar Avatar renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div>
6 | <img
7 | alt=""
8 | height="64"
9 | src=""
10 | style="outline: none; text-decoration: none; object-fit: cover; height: 64px; width: 64px; max-width: 100%; display: inline-block; vertical-align: middle; text-align: center;"
11 | width="64"
12 | />
13 | </div>
14 | </DocumentFragment>
15 | `;
16 |
--------------------------------------------------------------------------------
/packages/block-avatar/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Avatar } from '.';
6 |
7 | describe('block-avatar', () => {
8 | describe('Avatar', () => {
9 | it('renders with default values', () => {
10 | expect(render(<Avatar />).asFragment()).toMatchSnapshot();
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/packages/block-avatar/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | const PADDING_SCHEMA = z
5 | .object({
6 | top: z.number(),
7 | bottom: z.number(),
8 | right: z.number(),
9 | left: z.number(),
10 | })
11 | .optional()
12 | .nullable();
13 |
14 | const getPadding = (padding: z.infer<typeof PADDING_SCHEMA>) =>
15 | padding ? `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px` : undefined;
16 |
17 | export const AvatarPropsSchema = z.object({
18 | style: z
19 | .object({
20 | textAlign: z.enum(['left', 'center', 'right']).optional().nullable(),
21 | padding: PADDING_SCHEMA,
22 | })
23 | .optional()
24 | .nullable(),
25 | props: z
26 | .object({
27 | size: z.number().gt(0).optional().nullable(),
28 | shape: z.enum(['circle', 'square', 'rounded']).optional().nullable(),
29 | imageUrl: z.string().optional().nullable(),
30 | alt: z.string().optional().nullable(),
31 | })
32 | .optional()
33 | .nullable(),
34 | });
35 |
36 | export type AvatarProps = z.infer<typeof AvatarPropsSchema>;
37 |
38 | function getBorderRadius(shape: 'circle' | 'square' | 'rounded', size: number): number | undefined {
39 | switch (shape) {
40 | case 'rounded':
41 | return size * 0.125;
42 | case 'circle':
43 | return size;
44 | case 'square':
45 | default:
46 | return undefined;
47 | }
48 | }
49 |
50 | export const AvatarPropsDefaults = {
51 | size: 64,
52 | imageUrl: '',
53 | alt: '',
54 | shape: 'square',
55 | } as const;
56 |
57 | export function Avatar({ style, props }: AvatarProps) {
58 | const size = props?.size ?? AvatarPropsDefaults.size;
59 | const imageUrl = props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
60 | const alt = props?.alt ?? AvatarPropsDefaults.alt;
61 | const shape = props?.shape ?? AvatarPropsDefaults.shape;
62 |
63 | const sectionStyle: CSSProperties = {
64 | textAlign: style?.textAlign ?? undefined,
65 | padding: getPadding(style?.padding),
66 | };
67 | return (
68 | <div style={sectionStyle}>
69 | <img
70 | alt={alt}
71 | src={imageUrl}
72 | height={size}
73 | width={size}
74 | style={{
75 | outline: 'none',
76 | border: 'none',
77 | textDecoration: 'none',
78 | objectFit: 'cover',
79 | height: size,
80 | width: size,
81 | maxWidth: '100%',
82 | display: 'inline-block',
83 | verticalAlign: 'middle',
84 | textAlign: 'center',
85 | borderRadius: getBorderRadius(shape, size),
86 | }}
87 | />
88 | </div>
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/packages/block-avatar/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-avatar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-button/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-button/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-button/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-button
2 |
3 | Button component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-button/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-button",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible Button component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-button/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-button renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div>
6 | <a
7 | href=""
8 | style="color: rgb(255, 255, 255); font-size: 16px; font-weight: bold; background-color: rgb(153, 153, 153); border-radius: 4px; display: inline-block; padding: 12px 20px; text-decoration: none;"
9 | target="_blank"
10 | >
11 | <span>
12 | <!--[if mso]><i style="letter-spacing: 20px;mso-font-width:-100%;mso-text-raise:30" hidden> </i><![endif]-->
13 | </span>
14 | <span />
15 | <span>
16 | <!--[if mso]><i style="letter-spacing: 20px;mso-font-width:-100%" hidden> </i><![endif]-->
17 | </span>
18 | </a>
19 | </div>
20 | </DocumentFragment>
21 | `;
22 |
--------------------------------------------------------------------------------
/packages/block-button/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Button } from '.';
6 |
7 | describe('block-button', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Button />).asFragment()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/block-button/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-button/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-columns-container/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-columns-container/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-columns-container/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-columns-container
2 |
3 | ColumnsContainer component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-columns-container/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-columns-container",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible ColumnsContainer component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-columns-container/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { ColumnsContainer } from '.';
6 |
7 | describe('block-columns-container', () => {
8 | it('renders with default values', () => {
9 | expect(render(<ColumnsContainer />).asFragment()).toMatchSnapshot();
10 | });
11 |
12 | describe('columnsCount 2', () => {
13 | it('renders column children', () => {
14 | const columns = [<>bread</>, <>tomato</>, <>lettuce</>];
15 | expect(render(<ColumnsContainer props={{ columnsCount: 2 }} columns={columns} />).asFragment()).toMatchSnapshot();
16 | });
17 |
18 | it('uses padding correctly', () => {
19 | const columns = [<>bread</>, <>tomato</>, <>lettuce</>];
20 | expect(
21 | render(
22 | <ColumnsContainer
23 | props={{
24 | columnsGap: 12,
25 | columnsCount: 2,
26 | }}
27 | columns={columns}
28 | />
29 | ).asFragment()
30 | ).toMatchSnapshot();
31 | });
32 | });
33 |
34 | describe('columnsCount 3', () => {
35 | it('renders column children', () => {
36 | const columns = [<>bread</>, <>tomato</>, <>lettuce</>];
37 | expect(render(<ColumnsContainer props={{ columnsCount: 3 }} columns={columns} />).asFragment()).toMatchSnapshot();
38 | });
39 |
40 | it('uses padding correctly', () => {
41 | const columns = [<>bread</>, <>tomato</>, <>lettuce</>];
42 | expect(
43 | render(
44 | <ColumnsContainer
45 | props={{
46 | columnsGap: 12,
47 | columnsCount: 3,
48 | }}
49 | columns={columns}
50 | />
51 | ).asFragment()
52 | ).toMatchSnapshot();
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/packages/block-columns-container/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-columns-container/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-container/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-container/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-container/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-container
2 |
3 | Container component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-container/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-container",
3 | "version": "0.0.2",
4 | "description": "@usewaypoint/document compatible Container component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-container/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-container renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div />
6 | </DocumentFragment>
7 | `;
8 |
--------------------------------------------------------------------------------
/packages/block-container/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Container } from '.';
6 |
7 | describe('block-container', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Container />).asFragment()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/block-container/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | const COLOR_SCHEMA = z
5 | .string()
6 | .regex(/^#[0-9a-fA-F]{6}$/)
7 | .nullable()
8 | .optional();
9 |
10 | const PADDING_SCHEMA = z
11 | .object({
12 | top: z.number(),
13 | bottom: z.number(),
14 | right: z.number(),
15 | left: z.number(),
16 | })
17 | .optional()
18 | .nullable();
19 |
20 | const getPadding = (padding: z.infer<typeof PADDING_SCHEMA>) =>
21 | padding ? `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px` : undefined;
22 |
23 | export const ContainerPropsSchema = z.object({
24 | style: z
25 | .object({
26 | backgroundColor: COLOR_SCHEMA,
27 | borderColor: COLOR_SCHEMA,
28 | borderRadius: z.number().optional().nullable(),
29 | padding: PADDING_SCHEMA,
30 | })
31 | .optional()
32 | .nullable(),
33 | });
34 |
35 | export type ContainerProps = {
36 | style?: z.infer<typeof ContainerPropsSchema>['style'];
37 | children?: JSX.Element | JSX.Element[] | null;
38 | };
39 |
40 | function getBorder(style: ContainerProps['style']) {
41 | if (!style || !style.borderColor) {
42 | return undefined;
43 | }
44 | return `1px solid ${style.borderColor}`;
45 | }
46 |
47 | export function Container({ style, children }: ContainerProps) {
48 | const wStyle: CSSProperties = {
49 | backgroundColor: style?.backgroundColor ?? undefined,
50 | border: getBorder(style),
51 | borderRadius: style?.borderRadius ?? undefined,
52 | padding: getPadding(style?.padding),
53 | };
54 | if (!children) {
55 | return <div style={wStyle} />;
56 | }
57 | return <div style={wStyle}>{children}</div>;
58 | }
59 |
--------------------------------------------------------------------------------
/packages/block-container/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-container/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-divider/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-divider/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-divider/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-divider
2 |
3 | Divider component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-divider/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-divider",
3 | "version": "0.0.4",
4 | "description": "@usewaypoint/document compatible Divider component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-divider/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Divider renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div>
6 | <hr
7 | style="width: 100%; border-top: 1px solid #333333; margin: 0px;"
8 | />
9 | </div>
10 | </DocumentFragment>
11 | `;
12 |
13 | exports[`Divider renders with props 1`] = `
14 | <DocumentFragment>
15 | <div
16 | style="padding: 1px 4px 3px 2px; background-color: rgb(255, 240, 0);"
17 | >
18 | <hr
19 | style="width: 100%; border-top: 10px solid #444222; margin: 0px;"
20 | />
21 | </div>
22 | </DocumentFragment>
23 | `;
24 |
--------------------------------------------------------------------------------
/packages/block-divider/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Divider } from '.';
6 |
7 | describe('Divider', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Divider />).asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it('renders with props', () => {
13 | expect(
14 | render(
15 | <Divider
16 | style={{
17 | padding: { top: 1, left: 2, bottom: 3, right: 4 },
18 | backgroundColor: '#fff000',
19 | }}
20 | props={{ lineColor: '#444222', lineHeight: 10 }}
21 | />
22 | ).asFragment()
23 | ).toMatchSnapshot();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/block-divider/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | const COLOR_SCHEMA = z
5 | .string()
6 | .regex(/^#[0-9a-fA-F]{6}$/)
7 | .nullable()
8 | .optional();
9 |
10 | const PADDING_SCHEMA = z
11 | .object({
12 | top: z.number(),
13 | bottom: z.number(),
14 | right: z.number(),
15 | left: z.number(),
16 | })
17 | .optional()
18 | .nullable();
19 |
20 | const getPadding = (padding: z.infer<typeof PADDING_SCHEMA>) =>
21 | padding ? `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px` : undefined;
22 |
23 | export const DividerPropsSchema = z.object({
24 | style: z
25 | .object({
26 | backgroundColor: COLOR_SCHEMA,
27 | padding: PADDING_SCHEMA,
28 | })
29 | .optional()
30 | .nullable(),
31 | props: z
32 | .object({
33 | lineColor: COLOR_SCHEMA,
34 | lineHeight: z.number().optional().nullable(),
35 | })
36 | .optional()
37 | .nullable(),
38 | });
39 |
40 | export type DividerProps = z.infer<typeof DividerPropsSchema>;
41 |
42 | export const DividerPropsDefaults = {
43 | lineHeight: 1,
44 | lineColor: '#333333',
45 | };
46 |
47 | export function Divider({ style, props }: DividerProps) {
48 | const st: CSSProperties = {
49 | padding: getPadding(style?.padding),
50 | backgroundColor: style?.backgroundColor ?? undefined,
51 | };
52 | const borderTopWidth = props?.lineHeight ?? DividerPropsDefaults.lineHeight;
53 | const borderTopColor = props?.lineColor ?? DividerPropsDefaults.lineColor;
54 | return (
55 | <div style={st}>
56 | <hr
57 | style={{
58 | width: '100%',
59 | border: 'none',
60 | borderTop: `${borderTopWidth}px solid ${borderTopColor}`,
61 | margin: 0,
62 | }}
63 | />
64 | </div>
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/packages/block-divider/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-divider/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-heading/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-heading/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-heading/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-heading
2 |
3 | Heading component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-heading/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-heading",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible Heading component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-heading/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Heading renders with default values 1`] = `
4 | <DocumentFragment>
5 | <h2
6 | style="font-weight: bold; margin: 0px; font-size: 24px;"
7 | />
8 | </DocumentFragment>
9 | `;
10 |
11 | exports[`Heading renders with style 1`] = `
12 | <DocumentFragment>
13 | <h1
14 | style="color: rgb(16, 16, 16); background-color: rgb(68, 67, 51); font-weight: normal; text-align: center; margin: 0px; font-family: Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif; font-size: 32px; padding: 15px 8px 10px 24px;"
15 | >
16 | Hello world!
17 | </h1>
18 | </DocumentFragment>
19 | `;
20 |
--------------------------------------------------------------------------------
/packages/block-heading/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Heading } from '.';
6 |
7 | describe('Heading', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Heading />).asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it('renders with style', () => {
13 | const style = {
14 | backgroundColor: '#444333',
15 | color: '#101010',
16 | fontFamily: 'HEAVY_SANS' as const,
17 | fontWeight: 'normal' as const,
18 | padding: {
19 | top: 15,
20 | bottom: 10,
21 | left: 24,
22 | right: 8,
23 | },
24 | textAlign: 'center' as const,
25 | };
26 | const props = {
27 | text: 'Hello world!',
28 | level: 'h1' as const,
29 | };
30 | expect(render(<Heading style={style} props={props} />).asFragment()).toMatchSnapshot();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/block-heading/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-heading/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-html/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-html/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-html/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-html
2 |
3 | HTML component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-html/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-html",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible HTML component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-html/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-html renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div />
6 | </DocumentFragment>
7 | `;
8 |
--------------------------------------------------------------------------------
/packages/block-html/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Html } from '.';
6 |
7 | describe('block-html', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Html />).asFragment()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/block-html/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | const FONT_FAMILY_SCHEMA = z
5 | .enum([
6 | 'MODERN_SANS',
7 | 'BOOK_SANS',
8 | 'ORGANIC_SANS',
9 | 'GEOMETRIC_SANS',
10 | 'HEAVY_SANS',
11 | 'ROUNDED_SANS',
12 | 'MODERN_SERIF',
13 | 'BOOK_SERIF',
14 | 'MONOSPACE',
15 | ])
16 | .nullable()
17 | .optional();
18 |
19 | function getFontFamily(fontFamily: z.infer<typeof FONT_FAMILY_SCHEMA>) {
20 | switch (fontFamily) {
21 | case 'MODERN_SANS':
22 | return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif';
23 | case 'BOOK_SANS':
24 | return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif';
25 | case 'ORGANIC_SANS':
26 | return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif';
27 | case 'GEOMETRIC_SANS':
28 | return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif';
29 | case 'HEAVY_SANS':
30 | return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif';
31 | case 'ROUNDED_SANS':
32 | return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif';
33 | case 'MODERN_SERIF':
34 | return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif';
35 | case 'BOOK_SERIF':
36 | return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif';
37 | case 'MONOSPACE':
38 | return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace';
39 | }
40 | return undefined;
41 | }
42 |
43 | const COLOR_SCHEMA = z
44 | .string()
45 | .regex(/^#[0-9a-fA-F]{6}$/)
46 | .nullable()
47 | .optional();
48 |
49 | const PADDING_SCHEMA = z
50 | .object({
51 | top: z.number(),
52 | bottom: z.number(),
53 | right: z.number(),
54 | left: z.number(),
55 | })
56 | .optional()
57 | .nullable();
58 |
59 | const getPadding = (padding: z.infer<typeof PADDING_SCHEMA>) =>
60 | padding ? `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px` : undefined;
61 |
62 | export const HtmlPropsSchema = z.object({
63 | style: z
64 | .object({
65 | color: COLOR_SCHEMA,
66 | backgroundColor: COLOR_SCHEMA,
67 | fontFamily: FONT_FAMILY_SCHEMA,
68 | fontSize: z.number().min(0).optional().nullable(),
69 | textAlign: z.enum(['left', 'right', 'center']).optional().nullable(),
70 | padding: PADDING_SCHEMA,
71 | })
72 | .optional()
73 | .nullable(),
74 | props: z
75 | .object({
76 | contents: z.string().optional().nullable(),
77 | })
78 | .optional()
79 | .nullable(),
80 | });
81 |
82 | export type HtmlProps = z.infer<typeof HtmlPropsSchema>;
83 |
84 | export function Html({ style, props }: HtmlProps) {
85 | const children = props?.contents;
86 | const cssStyle: CSSProperties = {
87 | color: style?.color ?? undefined,
88 | backgroundColor: style?.backgroundColor ?? undefined,
89 | fontFamily: getFontFamily(style?.fontFamily),
90 | fontSize: style?.fontSize ?? undefined,
91 | textAlign: style?.textAlign ?? undefined,
92 | padding: getPadding(style?.padding),
93 | };
94 | if (!children) {
95 | return <div style={cssStyle} />;
96 | }
97 | return <div style={cssStyle} dangerouslySetInnerHTML={{ __html: children }} />;
98 | }
99 |
--------------------------------------------------------------------------------
/packages/block-html/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-html/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-image/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tests
9 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-image/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-image/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-image
2 |
3 | Image component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-image/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-image",
3 | "version": "0.0.5",
4 | "description": "@usewaypoint/document compatible Image component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-image/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-image renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div>
6 | <img
7 | alt=""
8 | src=""
9 | style="outline: none; text-decoration: none; vertical-align: middle; display: inline-block; max-width: 100%;"
10 | />
11 | </div>
12 | </DocumentFragment>
13 | `;
14 |
--------------------------------------------------------------------------------
/packages/block-image/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Image } from '.';
6 |
7 | describe('block-image', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Image />).asFragment()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/block-image/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | const PADDING_SCHEMA = z
5 | .object({
6 | top: z.number(),
7 | bottom: z.number(),
8 | right: z.number(),
9 | left: z.number(),
10 | })
11 | .optional()
12 | .nullable();
13 |
14 | const getPadding = (padding: z.infer<typeof PADDING_SCHEMA>) =>
15 | padding ? `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px` : undefined;
16 |
17 | export const ImagePropsSchema = z.object({
18 | style: z
19 | .object({
20 | padding: PADDING_SCHEMA,
21 | backgroundColor: z
22 | .string()
23 | .regex(/^#[0-9a-fA-F]{6}$/)
24 | .optional()
25 | .nullable(),
26 | textAlign: z.enum(['center', 'left', 'right']).optional().nullable(),
27 | })
28 | .optional()
29 | .nullable(),
30 | props: z
31 | .object({
32 | width: z.number().optional().nullable(),
33 | height: z.number().optional().nullable(),
34 | url: z.string().optional().nullable(),
35 | alt: z.string().optional().nullable(),
36 | linkHref: z.string().optional().nullable(),
37 | contentAlignment: z.enum(['top', 'middle', 'bottom']).optional().nullable(),
38 | })
39 | .optional()
40 | .nullable(),
41 | });
42 |
43 | export type ImageProps = z.infer<typeof ImagePropsSchema>;
44 |
45 | export function Image({ style, props }: ImageProps) {
46 | const sectionStyle: CSSProperties = {
47 | padding: getPadding(style?.padding),
48 | backgroundColor: style?.backgroundColor ?? undefined,
49 | textAlign: style?.textAlign ?? undefined,
50 | };
51 |
52 | const linkHref = props?.linkHref ?? null;
53 | const width = props?.width ?? undefined;
54 | const height = props?.height ?? undefined;
55 |
56 | const imageElement = (
57 | <img
58 | alt={props?.alt ?? ''}
59 | src={props?.url ?? ''}
60 | width={width}
61 | height={height}
62 | style={{
63 | width,
64 | height,
65 | outline: 'none',
66 | border: 'none',
67 | textDecoration: 'none',
68 | verticalAlign: props?.contentAlignment ?? 'middle',
69 | display: 'inline-block',
70 | maxWidth: '100%',
71 | }}
72 | />
73 | );
74 |
75 | if (!linkHref) {
76 | return <div style={sectionStyle}>{imageElement}</div>;
77 | }
78 |
79 | return (
80 | <div style={sectionStyle}>
81 | <a href={linkHref} style={{ textDecoration: 'none' }} target="_blank">
82 | {imageElement}
83 | </a>
84 | </div>
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/packages/block-image/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-image/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-spacer/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tests
9 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-spacer/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-spacer/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-spacer
2 |
3 | Spacer component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-spacer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-spacer",
3 | "version": "0.0.3",
4 | "description": "@usewaypoint/document compatible Spacer component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/block-spacer/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Spacer renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div
6 | style="height: 16px;"
7 | />
8 | </DocumentFragment>
9 | `;
10 |
11 | exports[`Spacer renders with props 1`] = `
12 | <DocumentFragment>
13 | <div
14 | style="height: 10px;"
15 | />
16 | </DocumentFragment>
17 | `;
18 |
--------------------------------------------------------------------------------
/packages/block-spacer/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Spacer } from '.';
6 |
7 | describe('Spacer', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Spacer />).asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it('renders with props', () => {
13 | expect(render(<Spacer props={{ height: 10 }} />).asFragment()).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/packages/block-spacer/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { z } from 'zod';
3 |
4 | export const SpacerPropsSchema = z.object({
5 | props: z
6 | .object({
7 | height: z.number().gte(0).optional().nullish(),
8 | })
9 | .optional()
10 | .nullable(),
11 | });
12 |
13 | export type SpacerProps = z.infer<typeof SpacerPropsSchema>;
14 |
15 | export const SpacerPropsDefaults = {
16 | height: 16,
17 | };
18 |
19 | export function Spacer({ props }: SpacerProps) {
20 | const style: CSSProperties = {
21 | height: props?.height ?? SpacerPropsDefaults.height,
22 | };
23 | return <div style={style} />;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/block-spacer/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-spacer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/block-text/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tsconfig.json
--------------------------------------------------------------------------------
/packages/block-text/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/block-text/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/block-text
2 |
3 | Text component for use with the EmailBuilder package.
4 |
--------------------------------------------------------------------------------
/packages/block-text/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/block-text",
3 | "version": "0.0.6",
4 | "description": "@usewaypoint/document compatible Text component",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | },
27 | "devDependencies": {
28 | "@testing-library/react": "^14.2.1",
29 | "@types/insane": "^1.0.0"
30 | },
31 | "dependencies": {
32 | "insane": "^2.6.2",
33 | "marked": "^12.0.2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/block-text/src/EmailMarkdown.tsx:
--------------------------------------------------------------------------------
1 | import insane, { AllowedTags } from 'insane';
2 | import { marked, Renderer } from 'marked';
3 | import React, { CSSProperties, useMemo } from 'react';
4 |
5 | const ALLOWED_TAGS: AllowedTags[] = [
6 | 'a',
7 | 'article',
8 | 'b',
9 | 'blockquote',
10 | 'br',
11 | 'caption',
12 | 'code',
13 | 'del',
14 | 'details',
15 | 'div',
16 | 'em',
17 | 'h1',
18 | 'h2',
19 | 'h3',
20 | 'h4',
21 | 'h5',
22 | 'h6',
23 | 'hr',
24 | 'i',
25 | 'img',
26 | 'ins',
27 | 'kbd',
28 | 'li',
29 | 'main',
30 | 'ol',
31 | 'p',
32 | 'pre',
33 | 'section',
34 | 'span',
35 | 'strong',
36 | 'sub',
37 | 'summary',
38 | 'sup',
39 | 'table',
40 | 'tbody',
41 | 'td',
42 | 'th',
43 | 'thead',
44 | 'tr',
45 | 'u',
46 | 'ul',
47 | ];
48 | const GENERIC_ALLOWED_ATTRIBUTES = ['style', 'title'];
49 |
50 | function sanitizer(html: string): string {
51 | return insane(html, {
52 | allowedTags: ALLOWED_TAGS,
53 | allowedAttributes: {
54 | ...ALLOWED_TAGS.reduce<Record<string, string[]>>((res, tag) => {
55 | res[tag] = [...GENERIC_ALLOWED_ATTRIBUTES];
56 | return res;
57 | }, {}),
58 | img: ['src', 'srcset', 'alt', 'width', 'height', ...GENERIC_ALLOWED_ATTRIBUTES],
59 | table: ['width', ...GENERIC_ALLOWED_ATTRIBUTES],
60 | td: ['align', 'width', ...GENERIC_ALLOWED_ATTRIBUTES],
61 | th: ['align', 'width', ...GENERIC_ALLOWED_ATTRIBUTES],
62 | a: ['href', 'target', ...GENERIC_ALLOWED_ATTRIBUTES],
63 | ol: ['start', ...GENERIC_ALLOWED_ATTRIBUTES],
64 | ul: ['start', ...GENERIC_ALLOWED_ATTRIBUTES],
65 | },
66 | });
67 | }
68 |
69 | class CustomRenderer extends Renderer {
70 | table(header: string, body: string) {
71 | return `<table width="100%">
72 | <thead>
73 | ${header}</thead>
74 | <tbody>
75 | ${body}</tbody>
76 | </table>`;
77 | }
78 |
79 | link(href: string, title: string | null, text: string) {
80 | if (!title) {
81 | return `<a href="${href}" target="_blank">${text}</a>`;
82 | }
83 | return `<a href="${href}" title="${title}" target="_blank">${text}</a>`;
84 | }
85 | }
86 |
87 | function renderMarkdownString(str: string): string {
88 | const html = marked.parse(str, {
89 | async: false,
90 | breaks: true,
91 | gfm: true,
92 | pedantic: false,
93 | silent: false,
94 | renderer: new CustomRenderer(),
95 | });
96 | if (typeof html !== 'string') {
97 | throw new Error('marked.parse did not return a string');
98 | }
99 | return sanitizer(html);
100 | }
101 |
102 | type Props = {
103 | style: CSSProperties;
104 | markdown: string;
105 | };
106 | export default function EmailMarkdown({ markdown, ...props }: Props) {
107 | const data = useMemo(() => renderMarkdownString(markdown), [markdown]);
108 | return <div {...props} dangerouslySetInnerHTML={{ __html: data }} />;
109 | }
110 |
--------------------------------------------------------------------------------
/packages/block-text/src/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`block-text renders with default values 1`] = `
4 | <DocumentFragment>
5 | <div />
6 | </DocumentFragment>
7 | `;
8 |
9 | exports[`block-text renders with safe markdown 1`] = `
10 | <DocumentFragment>
11 | <div>
12 | <p>
13 | This
14 | <span>
15 | text
16 | </span>
17 | block has the
18 | <strong>
19 | Markdown
20 | </strong>
21 | option
22 | <em>
23 | turned on
24 | </em>
25 | .
26 | </p>
27 |
28 |
29 | <ul>
30 |
31 |
32 | <li>
33 | One
34 | </li>
35 |
36 |
37 | <li>
38 | Two
39 | </li>
40 |
41 |
42 | <li>
43 | Three
44 | </li>
45 |
46 |
47 | </ul>
48 |
49 |
50 | <p>
51 | Powered by
52 | <a
53 | href="https://usewaypoint.com"
54 | target="_blank"
55 | >
56 | Waypoint
57 | </a>
58 | </p>
59 |
60 |
61 | </div>
62 | </DocumentFragment>
63 | `;
64 |
65 | exports[`block-text renders without markdown 1`] = `
66 | <DocumentFragment>
67 | <div>
68 | ## This is not <span>markdown</span>
69 | </div>
70 | </DocumentFragment>
71 | `;
72 |
73 | exports[`block-text sanitizes HTML 1`] = `
74 | <DocumentFragment>
75 | <div>
76 |
77 |
78 | <img
79 | src="x"
80 | />
81 |
82 |
83 |
84 | <p>
85 | <a
86 | target="_blank"
87 | >
88 | a
89 | </a>
90 | <br />
91 | <a
92 | target="_blank"
93 | >
94 | Basic
95 | </a>
96 | <br />
97 | <a
98 | target="_blank"
99 | >
100 | Local Storage
101 | </a>
102 | <br />
103 | <a
104 | target="_blank"
105 | >
106 | CaseInsensitive
107 | </a>
108 | <br />
109 | <a
110 | target="_blank"
111 | >
112 | URL
113 | </a>
114 | </p>
115 |
116 |
117 | <p>
118 | <a
119 | target="_blank"
120 | >
121 | In Quotes
122 | </a>
123 | <br />
124 | [a](j a v a s c r i p t:prompt(document.cookie))
125 | <br />
126 | <a
127 | target="_blank"
128 | >
129 | a
130 | </a>
131 | <br />
132 | <a
133 | target="_blank"
134 | >
135 | a
136 | </a>
137 | <br />
138 | <img
139 | alt="Uh oh..."
140 | src="%22onerror=%22alert('XSS')"
141 | />
142 | <br />
143 | <img
144 | alt="Uh oh..."
145 | src="https://www.example.com/image.png%22onload=%22alert('XSS')"
146 | />
147 | <br />
148 | <img
149 | alt="Escape SRC - onload"
150 | src="https://www.example.com/image.png%22onload=%22alert('ImageOnLoad')"
151 | />
152 | <br />
153 | <img
154 | alt="Escape SRC - onerror"
155 | src="%22onerror=%22alert('ImageOnError')"
156 | />
157 | </p>
158 |
159 |
160 | </div>
161 | </DocumentFragment>
162 | `;
163 |
--------------------------------------------------------------------------------
/packages/block-text/src/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | import { Text } from '.';
6 |
7 | describe('block-text', () => {
8 | it('renders with default values', () => {
9 | expect(render(<Text />).asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it('sanitizes HTML', () => {
13 | expect(
14 | render(
15 | <Text
16 | props={{
17 | markdown: true,
18 | text: `
19 | <script>alert(1)</script>
20 | <img src=x onerror=alert(1) />
21 |
22 | [a](javascript:prompt(document.cookie))
23 | [Basic](javascript:alert('Basic'))
24 | [Local Storage](javascript:alert(JSON.stringify(localStorage)))
25 | [CaseInsensitive](JaVaScRiPt:alert('CaseInsensitive'))
26 | [URL](javascript://www.google.com%0Aalert('URL'))
27 |
28 | [In Quotes]('javascript:alert("InQuotes")')
29 | [a](j a v a s c r i p t:prompt(document.cookie))
30 | [a](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)
31 | [a](javascript:window.onerror=alert;throw%201)
32 | )
33 | )
34 | )
35 | )
36 | `,
37 | }}
38 | />
39 | ).asFragment()
40 | ).toMatchSnapshot();
41 | });
42 |
43 | it('renders with safe markdown', () => {
44 | expect(
45 | render(
46 | <Text
47 | props={{
48 | text: `This <span onClick="alert('!')">text</span> block has the **Markdown** option *turned on*.
49 |
50 | - One
51 | - Two
52 | - Three
53 |
54 | Powered by [Waypoint](https://usewaypoint.com)`,
55 | markdown: true,
56 | }}
57 | />
58 | ).asFragment()
59 | ).toMatchSnapshot();
60 | });
61 |
62 | it('renders without markdown', () => {
63 | expect(
64 | render(
65 | <Text
66 | props={{
67 | text: `## This is not <span>markdown</span>`,
68 | }}
69 | />
70 | ).asFragment()
71 | ).toMatchSnapshot();
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/packages/block-text/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/block-text/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/document-core/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tests
9 | tsconfig.json
--------------------------------------------------------------------------------
/packages/document-core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/document-core/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/document-core
2 |
3 | This is the core library used to build the email messages at [Waypoint](https://www.usewaypoint.com). It is non-opinionated and light on dependencies so that it can be used to compose complex documents.
4 |
5 | > [!WARNING]
6 | > This library is still under development and the final interface is subject
7 | > to change
8 |
9 | ## Installation
10 |
11 | **Installation with npm**
12 |
13 | ```
14 | npm install --save @usewaypoint/document-core
15 | ```
16 |
17 | ## Usage
18 |
19 | The root of the library is the `DocumentBlocksDictionary` dictionary. This is a mapping of block names to an object with a zod schema and a corresponding React Component.
20 |
21 | ```
22 | const dictionary = {
23 | Alert: {
24 | schema: z.object({
25 | message: z.string(),
26 | }),
27 | Component: ({ message }: { message: string }) => {
28 | return <div>{message.toUpperCase()}</div>
29 | }
30 | }
31 | }
32 | ```
33 |
34 | This dictionary object is passed as an argument to the builder functions.
35 |
36 | ### `buildBlockComponent`
37 |
38 | ```
39 | const Block = buildBlockComponent(dictionary);
40 |
41 | <Block type="Alert" data={{message: 'Hello World' }} />
42 | ```
43 |
44 | ### `buildBlockConfigurationSchema`
45 |
46 | ```
47 | const Schema = buildBlockConfigurationSchema(dictionary);
48 |
49 | const parsedData = Schema.safeParse({
50 | type: 'Alert',
51 | data: { message: 'Hello World' },
52 | });
53 | ```
54 |
--------------------------------------------------------------------------------
/packages/document-core/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/document-core",
3 | "version": "0.0.6",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@usewaypoint/document-core",
9 | "version": "0.0.6",
10 | "license": "MIT",
11 | "peerDependencies": {
12 | "react": "^16 || ^17 || ^18",
13 | "zod": "^1 || ^2 || ^3"
14 | }
15 | },
16 | "node_modules/js-tokens": {
17 | "version": "4.0.0",
18 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
19 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
20 | "peer": true
21 | },
22 | "node_modules/loose-envify": {
23 | "version": "1.4.0",
24 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
25 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
26 | "peer": true,
27 | "dependencies": {
28 | "js-tokens": "^3.0.0 || ^4.0.0"
29 | },
30 | "bin": {
31 | "loose-envify": "cli.js"
32 | }
33 | },
34 | "node_modules/react": {
35 | "version": "18.2.0",
36 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
37 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
38 | "peer": true,
39 | "dependencies": {
40 | "loose-envify": "^1.1.0"
41 | },
42 | "engines": {
43 | "node": ">=0.10.0"
44 | }
45 | },
46 | "node_modules/zod": {
47 | "version": "3.22.4",
48 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
49 | "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
50 | "peer": true,
51 | "funding": {
52 | "url": "https://github.com/sponsors/colinhacks"
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/document-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/document-core",
3 | "version": "0.0.6",
4 | "description": "Tools to render waypoint-style documents (core package)",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "build": "npx tsc"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "zod": "^1 || ^2 || ^3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/document-core/src/builders/buildBlockComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { BaseZodDictionary, BlockConfiguration, DocumentBlocksDictionary } from '../utils';
4 |
5 | /**
6 | * @param blocks Main DocumentBlocksDictionary
7 | * @returns React component that can render a BlockConfiguration that is compatible with blocks
8 | */
9 | export default function buildBlockComponent<T extends BaseZodDictionary>(blocks: DocumentBlocksDictionary<T>) {
10 | return function BlockComponent({ type, data }: BlockConfiguration<T>) {
11 | const Component = blocks[type].Component;
12 | return <Component {...data} />;
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/packages/document-core/src/builders/buildBlockConfigurationDictionary.ts:
--------------------------------------------------------------------------------
1 | import { BaseZodDictionary, DocumentBlocksDictionary } from '../utils';
2 |
3 | /**
4 | * Identity function to type a DocumentBlocksDictionary
5 | * @param blocks Main DocumentBlocksDictionary
6 | * @returns typed DocumentBlocksDictionary
7 | */
8 | export default function buildBlockConfigurationDictionary<T extends BaseZodDictionary>(
9 | blocks: DocumentBlocksDictionary<T>
10 | ) {
11 | return blocks;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/document-core/src/builders/buildBlockConfigurationSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { BaseZodDictionary, BlockConfiguration, DocumentBlocksDictionary } from '../utils';
4 |
5 | /**
6 | *
7 | * @param blocks Main DocumentBlocksDictionary
8 | * @returns zod schema that can parse arbitary objects into a single BlockConfiguration
9 | */
10 | export default function buildBlockConfigurationSchema<T extends BaseZodDictionary>(
11 | blocks: DocumentBlocksDictionary<T>
12 | ) {
13 | const blockObjects = Object.keys(blocks).map((type: keyof T) =>
14 | z.object({
15 | type: z.literal(type),
16 | data: blocks[type].schema,
17 | })
18 | );
19 |
20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
21 | return z.discriminatedUnion('type', blockObjects as any).transform((v) => v as BlockConfiguration<T>);
22 | }
23 |
--------------------------------------------------------------------------------
/packages/document-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as buildBlockComponent } from './builders/buildBlockComponent';
2 | export { default as buildBlockConfigurationSchema } from './builders/buildBlockConfigurationSchema';
3 | export { default as buildBlockConfigurationDictionary } from './builders/buildBlockConfigurationDictionary';
4 |
5 | export { BlockConfiguration, DocumentBlocksDictionary } from './utils';
6 |
--------------------------------------------------------------------------------
/packages/document-core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export type BaseZodDictionary = { [name: string]: z.AnyZodObject };
4 | export type DocumentBlocksDictionary<T extends BaseZodDictionary> = {
5 | [K in keyof T]: {
6 | schema: T[K];
7 | Component: (props: z.infer<T[K]>) => JSX.Element;
8 | };
9 | };
10 |
11 | export type BlockConfiguration<T extends BaseZodDictionary> = {
12 | [TType in keyof T]: {
13 | type: TType;
14 | data: z.infer<T[TType]>;
15 | };
16 | }[keyof T];
17 |
18 | export class BlockNotFoundError extends Error {
19 | blockId: string;
20 | constructor(blockId: string) {
21 | super('Could not find a block with the given blockId');
22 | this.blockId = blockId;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/document-core/tests/builder/__snapshots__/buildBlockComponent.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`builders/buildBlockComponent renders the specified component 1`] = `
4 | <DocumentFragment>
5 | <div>
6 | TEST TEXT!
7 | </div>
8 | </DocumentFragment>
9 | `;
10 |
--------------------------------------------------------------------------------
/packages/document-core/tests/builder/buildBlockComponent.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { z } from 'zod';
3 |
4 | import { render } from '@testing-library/react';
5 |
6 | import buildBlockComponent from '../../src/builders/buildBlockComponent';
7 |
8 | describe('builders/buildBlockComponent', () => {
9 | it('renders the specified component', () => {
10 | const BlockComponent = buildBlockComponent({
11 | SampleBlock: {
12 | schema: z.object({ text: z.string() }),
13 | Component: ({ text }) => <div>{text.toUpperCase()}</div>,
14 | },
15 | });
16 | expect(render(<BlockComponent type="SampleBlock" data={{ text: 'Test text!' }} />).asFragment()).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/packages/document-core/tests/builder/buildBlockConfigurationSchema.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { z } from 'zod';
3 |
4 | import buildBlockConfigurationSchema from '../../src/builders/buildBlockConfigurationSchema';
5 |
6 | describe('builders/buildBlockConfigurationSchema', () => {
7 | it('builds a BlockConfiguration schema with an id, data, and type', () => {
8 | const blockConfigurationSchema = buildBlockConfigurationSchema({
9 | SampleBlock: {
10 | schema: z.object({ text: z.string() }),
11 | Component: ({ text }) => <div>{text.toUpperCase()}</div>,
12 | },
13 | });
14 | const parsedData = blockConfigurationSchema.safeParse({
15 | type: 'SampleBlock',
16 | data: { text: 'Test text!' },
17 | });
18 | expect(parsedData).toEqual({
19 | success: true,
20 | data: {
21 | type: 'SampleBlock',
22 | data: { text: 'Test text!' },
23 | },
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/document-core/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["tests/**/*.spec.ts", "tests/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/document-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor-sample/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/editor-sample/README.md:
--------------------------------------------------------------------------------
1 | # @usewaypoint/editor-sample
2 |
3 | Use this as a sample to self-host EmailBuilder.js.
4 |
5 | To run this locally, fork the repository and then in this directory run:
6 |
7 | - `npm install`
8 | - `npx vite`
9 |
10 | Once the server is running, open http://localhost:5173/email-builder-js/ in your browser.
11 |
--------------------------------------------------------------------------------
/packages/editor-sample/index.html:
--------------------------------------------------------------------------------
1 | <!doctype html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8" />
5 | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" />
6 | <link rel="icon" type="image/png" sizes="32x32" href="/src/favicon/favicon-32x32.png" />
7 | <link rel="icon" type="image/png" sizes="16x16" href="/src/favicon/favicon-16x16.png" />
8 | <meta name="viewport" content="width=900" />
9 | <meta name="description" content="EmailBuilder.js interactive playground. Brought to you by Waypoint." />
10 | <title>EmailBuilder.js — Free and Open Source Template Builder</title>
11 | <style>
12 | html {
13 | margin: 0px;
14 | height: 100vh;
15 | width: 100%;
16 | }
17 | body {
18 | min-height: 100vh;
19 | width: 100%;
20 | }
21 | #root {
22 | height: 100vh;
23 | width: 100%;
24 | }
25 | </style>
26 | </head>
27 | <body>
28 | <div id="root"></div>
29 | <script type="module" src="/src/main.tsx"></script>
30 | </body>
31 | </html>
32 |
--------------------------------------------------------------------------------
/packages/editor-sample/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/editor-sample",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "dependencies": {
11 | "@emotion/react": "^11.11.3",
12 | "@emotion/styled": "^11.11.0",
13 | "@mui/icons-material": "^5.15.10",
14 | "@mui/material": "^5.15.10",
15 | "@usewaypoint/block-avatar": "^0.0.3",
16 | "@usewaypoint/block-button": "^0.0.3",
17 | "@usewaypoint/block-columns-container": "^0.0.3",
18 | "@usewaypoint/block-container": "^0.0.2",
19 | "@usewaypoint/block-divider": "^0.0.4",
20 | "@usewaypoint/block-heading": "^0.0.3",
21 | "@usewaypoint/block-html": "^0.0.3",
22 | "@usewaypoint/block-image": "^0.0.5",
23 | "@usewaypoint/block-spacer": "^0.0.3",
24 | "@usewaypoint/block-text": "^0.0.6",
25 | "@usewaypoint/document-core": "^0.0.6",
26 | "@usewaypoint/email-builder": "^0.0.8",
27 | "highlight.js": "^11.9.0",
28 | "prettier": "^3.2.5",
29 | "react": "^18.2.0",
30 | "react-colorful": "^5.6.1",
31 | "react-dom": "^18.2.0",
32 | "zod": "^3.22.4",
33 | "zustand": "^4.5.1"
34 | },
35 | "devDependencies": {
36 | "@types/react": "^18.2.55",
37 | "@types/react-dom": "^18.2.19",
38 | "@typescript-eslint/eslint-plugin": "^6.21.0",
39 | "@typescript-eslint/parser": "^6.21.0",
40 | "@vitejs/plugin-react-swc": "^3.5.0",
41 | "eslint": "^8.56.0",
42 | "eslint-plugin-react-hooks": "^4.6.0",
43 | "eslint-plugin-react-refresh": "^0.4.5",
44 | "eslint-plugin-simple-import-sort": "^12.0.0",
45 | "typescript": "^5.2.2",
46 | "vite": "^5.1.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Typography } from '@mui/material';
4 |
5 | import { TEditorBlock } from '../../../documents/editor/core';
6 | import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
7 |
8 | import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
9 | import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
10 | import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
11 | import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
12 | import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
13 | import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
14 | import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
15 | import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
16 | import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
17 | import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
18 | import TextSidebarPanel from './input-panels/TextSidebarPanel';
19 |
20 | function renderMessage(val: string) {
21 | return (
22 | <Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
23 | <Typography color="text.secondary">{val}</Typography>
24 | </Box>
25 | );
26 | }
27 |
28 | export default function ConfigurationPanel() {
29 | const document = useDocument();
30 | const selectedBlockId = useSelectedBlockId();
31 |
32 | if (!selectedBlockId) {
33 | return renderMessage('Click on a block to inspect.');
34 | }
35 | const block = document[selectedBlockId];
36 | if (!block) {
37 | return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
38 | }
39 |
40 | const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
41 | const { data, type } = block;
42 | switch (type) {
43 | case 'Avatar':
44 | return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
45 | case 'Button':
46 | return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
47 | case 'ColumnsContainer':
48 | return (
49 | <ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
50 | );
51 | case 'Container':
52 | return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
53 | case 'Divider':
54 | return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
55 | case 'Heading':
56 | return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
57 | case 'Html':
58 | return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
59 | case 'Image':
60 | return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
61 | case 'EmailLayout':
62 | return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
63 | case 'Spacer':
64 | return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
65 | case 'Text':
66 | return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
67 | default:
68 | return <pre>{JSON.stringify(block, null, ' ')}</pre>;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/AvatarSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { AspectRatioOutlined } from '@mui/icons-material';
4 | import { ToggleButton } from '@mui/material';
5 | import { AvatarProps, AvatarPropsDefaults, AvatarPropsSchema } from '@usewaypoint/block-avatar';
6 |
7 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
8 | import RadioGroupInput from './helpers/inputs/RadioGroupInput';
9 | import SliderInput from './helpers/inputs/SliderInput';
10 | import TextInput from './helpers/inputs/TextInput';
11 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
12 |
13 | type AvatarSidebarPanelProps = {
14 | data: AvatarProps;
15 | setData: (v: AvatarProps) => void;
16 | };
17 | export default function AvatarSidebarPanel({ data, setData }: AvatarSidebarPanelProps) {
18 | const [, setErrors] = useState<Zod.ZodError | null>(null);
19 | const updateData = (d: unknown) => {
20 | const res = AvatarPropsSchema.safeParse(d);
21 | if (res.success) {
22 | setData(res.data);
23 | setErrors(null);
24 | } else {
25 | setErrors(res.error);
26 | }
27 | };
28 |
29 | const size = data.props?.size ?? AvatarPropsDefaults.size;
30 | const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
31 | const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
32 | const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
33 |
34 | return (
35 | <BaseSidebarPanel title="Avatar block">
36 | <SliderInput
37 | label="Size"
38 | iconLabel={<AspectRatioOutlined sx={{ color: 'text.secondary' }} />}
39 | units="px"
40 | step={3}
41 | min={32}
42 | max={256}
43 | defaultValue={size}
44 | onChange={(size) => {
45 | updateData({ ...data, props: { ...data.props, size } });
46 | }}
47 | />
48 | <RadioGroupInput
49 | label="Shape"
50 | defaultValue={shape}
51 | onChange={(shape) => {
52 | updateData({ ...data, props: { ...data.props, shape } });
53 | }}
54 | >
55 | <ToggleButton value="circle">Circle</ToggleButton>
56 | <ToggleButton value="square">Square</ToggleButton>
57 | <ToggleButton value="rounded">Rounded</ToggleButton>
58 | </RadioGroupInput>
59 | <TextInput
60 | label="Image URL"
61 | defaultValue={imageUrl}
62 | onChange={(imageUrl) => {
63 | updateData({ ...data, props: { ...data.props, imageUrl } });
64 | }}
65 | />
66 | <TextInput
67 | label="Alt text"
68 | defaultValue={alt}
69 | onChange={(alt) => {
70 | updateData({ ...data, props: { ...data.props, alt } });
71 | }}
72 | />
73 |
74 | <MultiStylePropertyPanel
75 | names={['textAlign', 'padding']}
76 | value={data.style}
77 | onChange={(style) => updateData({ ...data, style })}
78 | />
79 | </BaseSidebarPanel>
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ColumnsContainerSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {
4 | SpaceBarOutlined,
5 | VerticalAlignBottomOutlined,
6 | VerticalAlignCenterOutlined,
7 | VerticalAlignTopOutlined,
8 | } from '@mui/icons-material';
9 | import { ToggleButton } from '@mui/material';
10 |
11 | import ColumnsContainerPropsSchema, {
12 | ColumnsContainerProps,
13 | } from '../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema';
14 |
15 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
16 | import ColumnWidthsInput from './helpers/inputs/ColumnWidthsInput';
17 | import RadioGroupInput from './helpers/inputs/RadioGroupInput';
18 | import SliderInput from './helpers/inputs/SliderInput';
19 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
20 |
21 | type ColumnsContainerPanelProps = {
22 | data: ColumnsContainerProps;
23 | setData: (v: ColumnsContainerProps) => void;
24 | };
25 | export default function ColumnsContainerPanel({ data, setData }: ColumnsContainerPanelProps) {
26 | const [, setErrors] = useState<Zod.ZodError | null>(null);
27 | const updateData = (d: unknown) => {
28 | const res = ColumnsContainerPropsSchema.safeParse(d);
29 | if (res.success) {
30 | setData(res.data);
31 | setErrors(null);
32 | } else {
33 | setErrors(res.error);
34 | }
35 | };
36 |
37 | return (
38 | <BaseSidebarPanel title="Columns block">
39 | <RadioGroupInput
40 | label="Number of columns"
41 | defaultValue={data.props?.columnsCount === 2 ? '2' : '3'}
42 | onChange={(v) => {
43 | updateData({ ...data, props: { ...data.props, columnsCount: v === '2' ? 2 : 3 } });
44 | }}
45 | >
46 | <ToggleButton value="2">2</ToggleButton>
47 | <ToggleButton value="3">3</ToggleButton>
48 | </RadioGroupInput>
49 | <ColumnWidthsInput
50 | defaultValue={data.props?.fixedWidths}
51 | onChange={(fixedWidths) => {
52 | updateData({ ...data, props: { ...data.props, fixedWidths } });
53 | }}
54 | />
55 | <SliderInput
56 | label="Columns gap"
57 | iconLabel={<SpaceBarOutlined sx={{ color: 'text.secondary' }} />}
58 | units="px"
59 | step={4}
60 | marks
61 | min={0}
62 | max={80}
63 | defaultValue={data.props?.columnsGap ?? 0}
64 | onChange={(columnsGap) => updateData({ ...data, props: { ...data.props, columnsGap } })}
65 | />
66 | <RadioGroupInput
67 | label="Alignment"
68 | defaultValue={data.props?.contentAlignment ?? 'middle'}
69 | onChange={(contentAlignment) => {
70 | updateData({ ...data, props: { ...data.props, contentAlignment } });
71 | }}
72 | >
73 | <ToggleButton value="top">
74 | <VerticalAlignTopOutlined fontSize="small" />
75 | </ToggleButton>
76 | <ToggleButton value="middle">
77 | <VerticalAlignCenterOutlined fontSize="small" />
78 | </ToggleButton>
79 | <ToggleButton value="bottom">
80 | <VerticalAlignBottomOutlined fontSize="small" />
81 | </ToggleButton>
82 | </RadioGroupInput>
83 |
84 | <MultiStylePropertyPanel
85 | names={['backgroundColor', 'padding']}
86 | value={data.style}
87 | onChange={(style) => updateData({ ...data, style })}
88 | />
89 | </BaseSidebarPanel>
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ContainerSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import ContainerPropsSchema, { ContainerProps } from '../../../../documents/blocks/Container/ContainerPropsSchema';
4 |
5 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
6 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
7 |
8 | type ContainerSidebarPanelProps = {
9 | data: ContainerProps;
10 | setData: (v: ContainerProps) => void;
11 | };
12 |
13 | export default function ContainerSidebarPanel({ data, setData }: ContainerSidebarPanelProps) {
14 | const [, setErrors] = useState<Zod.ZodError | null>(null);
15 | const updateData = (d: unknown) => {
16 | const res = ContainerPropsSchema.safeParse(d);
17 | if (res.success) {
18 | setData(res.data);
19 | setErrors(null);
20 | } else {
21 | setErrors(res.error);
22 | }
23 | };
24 | return (
25 | <BaseSidebarPanel title="Container block">
26 | <MultiStylePropertyPanel
27 | names={['backgroundColor', 'borderColor', 'borderRadius', 'padding']}
28 | value={data.style}
29 | onChange={(style) => updateData({ ...data, style })}
30 | />
31 | </BaseSidebarPanel>
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/DividerSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { HeightOutlined } from '@mui/icons-material';
4 | import { DividerProps, DividerPropsDefaults, DividerPropsSchema } from '@usewaypoint/block-divider';
5 |
6 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
7 | import ColorInput from './helpers/inputs/ColorInput';
8 | import SliderInput from './helpers/inputs/SliderInput';
9 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
10 |
11 | type DividerSidebarPanelProps = {
12 | data: DividerProps;
13 | setData: (v: DividerProps) => void;
14 | };
15 | export default function DividerSidebarPanel({ data, setData }: DividerSidebarPanelProps) {
16 | const [, setErrors] = useState<Zod.ZodError | null>(null);
17 | const updateData = (d: unknown) => {
18 | const res = DividerPropsSchema.safeParse(d);
19 | if (res.success) {
20 | setData(res.data);
21 | setErrors(null);
22 | } else {
23 | setErrors(res.error);
24 | }
25 | };
26 |
27 | const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
28 | const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
29 |
30 | return (
31 | <BaseSidebarPanel title="Divider block">
32 | <ColorInput
33 | label="Color"
34 | defaultValue={lineColor}
35 | onChange={(lineColor) => updateData({ ...data, props: { ...data.props, lineColor } })}
36 | />
37 | <SliderInput
38 | label="Height"
39 | iconLabel={<HeightOutlined sx={{ color: 'text.secondary' }} />}
40 | units="px"
41 | step={1}
42 | min={1}
43 | max={24}
44 | defaultValue={lineHeight}
45 | onChange={(lineHeight) => updateData({ ...data, props: { ...data.props, lineHeight } })}
46 | />
47 | <MultiStylePropertyPanel
48 | names={['backgroundColor', 'padding']}
49 | value={data.style}
50 | onChange={(style) => updateData({ ...data, style })}
51 | />
52 | </BaseSidebarPanel>
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/EmailLayoutSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { RoundedCornerOutlined } from '@mui/icons-material';
4 |
5 | import EmailLayoutPropsSchema, {
6 | EmailLayoutProps,
7 | } from '../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema';
8 |
9 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
10 | import ColorInput, { NullableColorInput } from './helpers/inputs/ColorInput';
11 | import { NullableFontFamily } from './helpers/inputs/FontFamily';
12 | import SliderInput from './helpers/inputs/SliderInput';
13 |
14 | type EmailLayoutSidebarFieldsProps = {
15 | data: EmailLayoutProps;
16 | setData: (v: EmailLayoutProps) => void;
17 | };
18 | export default function EmailLayoutSidebarFields({ data, setData }: EmailLayoutSidebarFieldsProps) {
19 | const [, setErrors] = useState<Zod.ZodError | null>(null);
20 |
21 | const updateData = (d: unknown) => {
22 | const res = EmailLayoutPropsSchema.safeParse(d);
23 | if (res.success) {
24 | setData(res.data);
25 | setErrors(null);
26 | } else {
27 | setErrors(res.error);
28 | }
29 | };
30 |
31 | return (
32 | <BaseSidebarPanel title="Global">
33 | <ColorInput
34 | label="Backdrop color"
35 | defaultValue={data.backdropColor ?? '#F5F5F5'}
36 | onChange={(backdropColor) => updateData({ ...data, backdropColor })}
37 | />
38 | <ColorInput
39 | label="Canvas color"
40 | defaultValue={data.canvasColor ?? '#FFFFFF'}
41 | onChange={(canvasColor) => updateData({ ...data, canvasColor })}
42 | />
43 | <NullableColorInput
44 | label="Canvas border color"
45 | defaultValue={data.borderColor ?? null}
46 | onChange={(borderColor) => updateData({ ...data, borderColor })}
47 | />
48 | <SliderInput
49 | iconLabel={<RoundedCornerOutlined />}
50 | units="px"
51 | step={4}
52 | marks
53 | min={0}
54 | max={48}
55 | label="Canvas border radius"
56 | defaultValue={data.borderRadius ?? 0}
57 | onChange={(borderRadius) => updateData({ ...data, borderRadius })}
58 | />
59 | <NullableFontFamily
60 | label="Font family"
61 | defaultValue="MODERN_SANS"
62 | onChange={(fontFamily) => updateData({ ...data, fontFamily })}
63 | />
64 | <ColorInput
65 | label="Text color"
66 | defaultValue={data.textColor ?? '#262626'}
67 | onChange={(textColor) => updateData({ ...data, textColor })}
68 | />
69 | </BaseSidebarPanel>
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/HeadingSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { ToggleButton } from '@mui/material';
4 | import { HeadingProps, HeadingPropsDefaults, HeadingPropsSchema } from '@usewaypoint/block-heading';
5 |
6 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
7 | import RadioGroupInput from './helpers/inputs/RadioGroupInput';
8 | import TextInput from './helpers/inputs/TextInput';
9 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
10 |
11 | type HeadingSidebarPanelProps = {
12 | data: HeadingProps;
13 | setData: (v: HeadingProps) => void;
14 | };
15 | export default function HeadingSidebarPanel({ data, setData }: HeadingSidebarPanelProps) {
16 | const [, setErrors] = useState<Zod.ZodError | null>(null);
17 |
18 | const updateData = (d: unknown) => {
19 | const res = HeadingPropsSchema.safeParse(d);
20 | if (res.success) {
21 | setData(res.data);
22 | setErrors(null);
23 | } else {
24 | setErrors(res.error);
25 | }
26 | };
27 |
28 | return (
29 | <BaseSidebarPanel title="Heading block">
30 | <TextInput
31 | label="Content"
32 | rows={3}
33 | defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
34 | onChange={(text) => {
35 | updateData({ ...data, props: { ...data.props, text } });
36 | }}
37 | />
38 | <RadioGroupInput
39 | label="Level"
40 | defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
41 | onChange={(level) => {
42 | updateData({ ...data, props: { ...data.props, level } });
43 | }}
44 | >
45 | <ToggleButton value="h1">H1</ToggleButton>
46 | <ToggleButton value="h2">H2</ToggleButton>
47 | <ToggleButton value="h3">H3</ToggleButton>
48 | </RadioGroupInput>
49 | <MultiStylePropertyPanel
50 | names={['color', 'backgroundColor', 'fontFamily', 'fontWeight', 'textAlign', 'padding']}
51 | value={data.style}
52 | onChange={(style) => updateData({ ...data, style })}
53 | />
54 | </BaseSidebarPanel>
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/HtmlSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { HtmlProps, HtmlPropsSchema } from '@usewaypoint/block-html';
4 |
5 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
6 | import TextInput from './helpers/inputs/TextInput';
7 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
8 |
9 | type HtmlSidebarPanelProps = {
10 | data: HtmlProps;
11 | setData: (v: HtmlProps) => void;
12 | };
13 | export default function HtmlSidebarPanel({ data, setData }: HtmlSidebarPanelProps) {
14 | const [, setErrors] = useState<Zod.ZodError | null>(null);
15 |
16 | const updateData = (d: unknown) => {
17 | const res = HtmlPropsSchema.safeParse(d);
18 | if (res.success) {
19 | setData(res.data);
20 | setErrors(null);
21 | } else {
22 | setErrors(res.error);
23 | }
24 | };
25 |
26 | return (
27 | <BaseSidebarPanel title="Html block">
28 | <TextInput
29 | label="Content"
30 | rows={5}
31 | defaultValue={data.props?.contents ?? ''}
32 | onChange={(contents) => updateData({ ...data, props: { ...data.props, contents } })}
33 | />
34 | <MultiStylePropertyPanel
35 | names={['color', 'backgroundColor', 'fontFamily', 'fontSize', 'textAlign', 'padding']}
36 | value={data.style}
37 | onChange={(style) => updateData({ ...data, style })}
38 | />
39 | </BaseSidebarPanel>
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/ImageSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {
4 | VerticalAlignBottomOutlined,
5 | VerticalAlignCenterOutlined,
6 | VerticalAlignTopOutlined,
7 | } from '@mui/icons-material';
8 | import { Stack, ToggleButton } from '@mui/material';
9 | import { ImageProps, ImagePropsSchema } from '@usewaypoint/block-image';
10 |
11 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
12 | import RadioGroupInput from './helpers/inputs/RadioGroupInput';
13 | import TextDimensionInput from './helpers/inputs/TextDimensionInput';
14 | import TextInput from './helpers/inputs/TextInput';
15 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
16 |
17 | type ImageSidebarPanelProps = {
18 | data: ImageProps;
19 | setData: (v: ImageProps) => void;
20 | };
21 | export default function ImageSidebarPanel({ data, setData }: ImageSidebarPanelProps) {
22 | const [, setErrors] = useState<Zod.ZodError | null>(null);
23 |
24 | const updateData = (d: unknown) => {
25 | const res = ImagePropsSchema.safeParse(d);
26 | if (res.success) {
27 | setData(res.data);
28 | setErrors(null);
29 | } else {
30 | setErrors(res.error);
31 | }
32 | };
33 |
34 | return (
35 | <BaseSidebarPanel title="Image block">
36 | <TextInput
37 | label="Source URL"
38 | defaultValue={data.props?.url ?? ''}
39 | onChange={(v) => {
40 | const url = v.trim().length === 0 ? null : v.trim();
41 | updateData({ ...data, props: { ...data.props, url } });
42 | }}
43 | />
44 |
45 | <TextInput
46 | label="Alt text"
47 | defaultValue={data.props?.alt ?? ''}
48 | onChange={(alt) => updateData({ ...data, props: { ...data.props, alt } })}
49 | />
50 | <TextInput
51 | label="Click through URL"
52 | defaultValue={data.props?.linkHref ?? ''}
53 | onChange={(v) => {
54 | const linkHref = v.trim().length === 0 ? null : v.trim();
55 | updateData({ ...data, props: { ...data.props, linkHref } });
56 | }}
57 | />
58 | <Stack direction="row" spacing={2}>
59 | <TextDimensionInput
60 | label="Width"
61 | defaultValue={data.props?.width}
62 | onChange={(width) => updateData({ ...data, props: { ...data.props, width } })}
63 | />
64 | <TextDimensionInput
65 | label="Height"
66 | defaultValue={data.props?.height}
67 | onChange={(height) => updateData({ ...data, props: { ...data.props, height } })}
68 | />
69 | </Stack>
70 |
71 | <RadioGroupInput
72 | label="Alignment"
73 | defaultValue={data.props?.contentAlignment ?? 'middle'}
74 | onChange={(contentAlignment) => updateData({ ...data, props: { ...data.props, contentAlignment } })}
75 | >
76 | <ToggleButton value="top">
77 | <VerticalAlignTopOutlined fontSize="small" />
78 | </ToggleButton>
79 | <ToggleButton value="middle">
80 | <VerticalAlignCenterOutlined fontSize="small" />
81 | </ToggleButton>
82 | <ToggleButton value="bottom">
83 | <VerticalAlignBottomOutlined fontSize="small" />
84 | </ToggleButton>
85 | </RadioGroupInput>
86 |
87 | <MultiStylePropertyPanel
88 | names={['backgroundColor', 'textAlign', 'padding']}
89 | value={data.style}
90 | onChange={(style) => updateData({ ...data, style })}
91 | />
92 | </BaseSidebarPanel>
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/SpacerSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { HeightOutlined } from '@mui/icons-material';
4 | import { SpacerProps, SpacerPropsDefaults, SpacerPropsSchema } from '@usewaypoint/block-spacer';
5 |
6 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
7 | import SliderInput from './helpers/inputs/SliderInput';
8 |
9 | type SpacerSidebarPanelProps = {
10 | data: SpacerProps;
11 | setData: (v: SpacerProps) => void;
12 | };
13 | export default function SpacerSidebarPanel({ data, setData }: SpacerSidebarPanelProps) {
14 | const [, setErrors] = useState<Zod.ZodError | null>(null);
15 |
16 | const updateData = (d: unknown) => {
17 | const res = SpacerPropsSchema.safeParse(d);
18 | if (res.success) {
19 | setData(res.data);
20 | setErrors(null);
21 | } else {
22 | setErrors(res.error);
23 | }
24 | };
25 |
26 | return (
27 | <BaseSidebarPanel title="Spacer block">
28 | <SliderInput
29 | label="Height"
30 | iconLabel={<HeightOutlined sx={{ color: 'text.secondary' }} />}
31 | units="px"
32 | step={4}
33 | min={4}
34 | max={128}
35 | defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
36 | onChange={(height) => updateData({ ...data, props: { ...data.props, height } })}
37 | />
38 | </BaseSidebarPanel>
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/TextSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { TextProps, TextPropsSchema } from '@usewaypoint/block-text';
4 |
5 | import BaseSidebarPanel from './helpers/BaseSidebarPanel';
6 | import BooleanInput from './helpers/inputs/BooleanInput';
7 | import TextInput from './helpers/inputs/TextInput';
8 | import MultiStylePropertyPanel from './helpers/style-inputs/MultiStylePropertyPanel';
9 |
10 | type TextSidebarPanelProps = {
11 | data: TextProps;
12 | setData: (v: TextProps) => void;
13 | };
14 | export default function TextSidebarPanel({ data, setData }: TextSidebarPanelProps) {
15 | const [, setErrors] = useState<Zod.ZodError | null>(null);
16 |
17 | const updateData = (d: unknown) => {
18 | const res = TextPropsSchema.safeParse(d);
19 | if (res.success) {
20 | setData(res.data);
21 | setErrors(null);
22 | } else {
23 | setErrors(res.error);
24 | }
25 | };
26 |
27 | return (
28 | <BaseSidebarPanel title="Text block">
29 | <TextInput
30 | label="Content"
31 | rows={5}
32 | defaultValue={data.props?.text ?? ''}
33 | onChange={(text) => updateData({ ...data, props: { ...data.props, text } })}
34 | />
35 | <BooleanInput
36 | label="Markdown"
37 | defaultValue={data.props?.markdown ?? false}
38 | onChange={(markdown) => updateData({ ...data, props: { ...data.props, markdown } })}
39 | />
40 |
41 | <MultiStylePropertyPanel
42 | names={['color', 'backgroundColor', 'fontFamily', 'fontSize', 'fontWeight', 'textAlign', 'padding']}
43 | value={data.style}
44 | onChange={(style) => updateData({ ...data, style })}
45 | />
46 | </BaseSidebarPanel>
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/BaseSidebarPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Stack, Typography } from '@mui/material';
4 |
5 | type SidebarPanelProps = {
6 | title: string;
7 | children: React.ReactNode;
8 | };
9 | export default function BaseSidebarPanel({ title, children }: SidebarPanelProps) {
10 | return (
11 | <Box p={2}>
12 | <Typography variant="overline" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
13 | {title}
14 | </Typography>
15 | <Stack spacing={5} mb={3}>
16 | {children}
17 | </Stack>
18 | </Box>
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/BooleanInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { FormControlLabel, Switch } from '@mui/material';
4 |
5 | type Props = {
6 | label: string;
7 | defaultValue: boolean;
8 | onChange: (value: boolean) => void;
9 | };
10 |
11 | export default function BooleanInput({ label, defaultValue, onChange }: Props) {
12 | const [value, setValue] = useState(defaultValue);
13 | return (
14 | <FormControlLabel
15 | label={label}
16 | control={
17 | <Switch
18 | checked={value}
19 | onChange={(_, checked: boolean) => {
20 | setValue(checked);
21 | onChange(checked);
22 | }}
23 | />
24 | }
25 | />
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/ColorInput/BaseColorInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { AddOutlined, CloseOutlined } from '@mui/icons-material';
4 | import { ButtonBase, InputLabel, Menu, Stack } from '@mui/material';
5 |
6 | import Picker from './Picker';
7 |
8 | const BUTTON_SX = {
9 | border: '1px solid',
10 | borderColor: 'cadet.400',
11 | width: 32,
12 | height: 32,
13 | borderRadius: '4px',
14 | bgcolor: '#FFFFFF',
15 | };
16 |
17 | type Props =
18 | | {
19 | nullable: true;
20 | label: string;
21 | onChange: (value: string | null) => void;
22 | defaultValue: string | null;
23 | }
24 | | {
25 | nullable: false;
26 | label: string;
27 | onChange: (value: string) => void;
28 | defaultValue: string;
29 | };
30 | export default function ColorInput({ label, defaultValue, onChange, nullable }: Props) {
31 | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
32 | const [value, setValue] = useState(defaultValue);
33 | const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
34 | setAnchorEl(event.currentTarget);
35 | };
36 |
37 | const renderResetButton = () => {
38 | if (!nullable) {
39 | return null;
40 | }
41 | if (typeof value !== 'string' || value.trim().length === 0) {
42 | return null;
43 | }
44 | return (
45 | <ButtonBase
46 | onClick={() => {
47 | setValue(null);
48 | onChange(null);
49 | }}
50 | >
51 | <CloseOutlined fontSize="small" sx={{ color: 'grey.600' }} />
52 | </ButtonBase>
53 | );
54 | };
55 |
56 | const renderOpenButton = () => {
57 | if (value) {
58 | return <ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX, bgcolor: value }} />;
59 | }
60 | return (
61 | <ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
62 | <AddOutlined fontSize="small" />
63 | </ButtonBase>
64 | );
65 | };
66 |
67 | return (
68 | <Stack alignItems="flex-start">
69 | <InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
70 | <Stack direction="row" spacing={1}>
71 | {renderOpenButton()}
72 | {renderResetButton()}
73 | </Stack>
74 | <Menu
75 | anchorEl={anchorEl}
76 | open={Boolean(anchorEl)}
77 | onClose={() => setAnchorEl(null)}
78 | MenuListProps={{
79 | sx: { height: 'auto', padding: 0 },
80 | }}
81 | >
82 | <Picker
83 | value={value || ''}
84 | onChange={(v) => {
85 | setValue(v);
86 | onChange(v);
87 | }}
88 | />
89 | </Menu>
90 | </Stack>
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/ColorInput/Picker.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HexColorInput, HexColorPicker } from 'react-colorful';
3 |
4 | import { Box, Stack, SxProps } from '@mui/material';
5 |
6 | import Swatch from './Swatch';
7 |
8 | const DEFAULT_PRESET_COLORS = [
9 | '#E11D48',
10 | '#DB2777',
11 | '#C026D3',
12 | '#9333EA',
13 | '#7C3AED',
14 | '#4F46E5',
15 | '#2563EB',
16 | '#0284C7',
17 | '#0891B2',
18 | '#0D9488',
19 | '#059669',
20 | '#16A34A',
21 | '#65A30D',
22 | '#CA8A04',
23 | '#D97706',
24 | '#EA580C',
25 | '#DC2626',
26 | '#FFFFFF',
27 | '#FAFAFA',
28 | '#F5F5F5',
29 | '#E5E5E5',
30 | '#D4D4D4',
31 | '#A3A3A3',
32 | '#737373',
33 | '#525252',
34 | '#404040',
35 | '#262626',
36 | '#171717',
37 | '#0A0A0A',
38 | '#000000',
39 | ];
40 |
41 | const SX: SxProps = {
42 | p: 1,
43 | '.react-colorful__pointer ': {
44 | width: 16,
45 | height: 16,
46 | },
47 | '.react-colorful__saturation': {
48 | mb: 1,
49 | borderRadius: '4px',
50 | },
51 | '.react-colorful__last-control': {
52 | borderRadius: '4px',
53 | },
54 | '.react-colorful__hue-pointer': {
55 | width: '4px',
56 | borderRadius: '4px',
57 | height: 24,
58 | cursor: 'col-resize',
59 | },
60 | '.react-colorful__saturation-pointer': {
61 | cursor: 'all-scroll',
62 | },
63 | input: {
64 | padding: 1,
65 | border: '1px solid',
66 | borderColor: 'grey.300',
67 | borderRadius: '4px',
68 | width: '100%',
69 | },
70 | };
71 |
72 | type Props = {
73 | value: string;
74 | onChange: (v: string) => void;
75 | };
76 | export default function Picker({ value, onChange }: Props) {
77 | return (
78 | <Stack spacing={1} sx={SX}>
79 | <HexColorPicker color={value} onChange={onChange} />
80 | <Swatch paletteColors={DEFAULT_PRESET_COLORS} value={value} onChange={onChange} />
81 | <Box pt={1}>
82 | <HexColorInput prefixed color={value} onChange={onChange} />
83 | </Box>
84 | </Stack>
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/ColorInput/Swatch.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Button, SxProps } from '@mui/material';
4 |
5 | type Props = {
6 | paletteColors: string[];
7 | value: string;
8 | onChange: (value: string) => void;
9 | };
10 |
11 | const TILE_BUTTON: SxProps = {
12 | width: 24,
13 | height: 24,
14 | };
15 | export default function Swatch({ paletteColors, value, onChange }: Props) {
16 | const renderButton = (colorValue: string) => {
17 | return (
18 | <Button
19 | key={colorValue}
20 | onClick={() => onChange(colorValue)}
21 | sx={{
22 | ...TILE_BUTTON,
23 | backgroundColor: colorValue,
24 | border: '1px solid',
25 | borderColor: value === colorValue ? 'black' : 'grey.200',
26 | minWidth: 24,
27 | display: 'inline-flex',
28 | '&:hover': {
29 | backgroundColor: colorValue,
30 | borderColor: 'grey.500',
31 | },
32 | }}
33 | />
34 | );
35 | };
36 | return (
37 | <Box width="100%" sx={{ display: 'grid', gap: 1, gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr 1fr' }}>
38 | {paletteColors.map((c) => renderButton(c))}
39 | </Box>
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/ColorInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import BaseColorInput from './BaseColorInput';
4 |
5 | type Props = {
6 | label: string;
7 | onChange: (value: string) => void;
8 | defaultValue: string;
9 | };
10 | export default function ColorInput(props: Props) {
11 | return <BaseColorInput {...props} nullable={false} />;
12 | }
13 |
14 | type NullableProps = {
15 | label: string;
16 | onChange: (value: null | string) => void;
17 | defaultValue: null | string;
18 | };
19 | export function NullableColorInput(props: NullableProps) {
20 | return <BaseColorInput {...props} nullable />;
21 | }
22 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/ColumnWidthsInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { Stack } from '@mui/material';
4 |
5 | import TextDimensionInput from './TextDimensionInput';
6 |
7 | export const DEFAULT_2_COLUMNS = [6] as [number];
8 | export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
9 |
10 | type TWidthValue = number | null | undefined;
11 | type FixedWidths = [
12 | //
13 | number | null | undefined,
14 | number | null | undefined,
15 | number | null | undefined,
16 | ];
17 | type ColumnsLayoutInputProps = {
18 | defaultValue: FixedWidths | null | undefined;
19 | onChange: (v: FixedWidths | null | undefined) => void;
20 | };
21 | export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
22 | const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
23 | if (defaultValue) {
24 | return defaultValue;
25 | }
26 | return [null, null, null];
27 | });
28 |
29 | const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
30 | const nValue: FixedWidths = [...currentValue];
31 | nValue[index] = value;
32 | setCurrentValue(nValue);
33 | onChange(nValue);
34 | };
35 |
36 | const columnsCountValue = 3;
37 | let column3 = null;
38 | if (columnsCountValue === 3) {
39 | column3 = (
40 | <TextDimensionInput
41 | label="Column 3"
42 | defaultValue={currentValue?.[2]}
43 | onChange={(v) => {
44 | setIndexValue(2, v);
45 | }}
46 | />
47 | );
48 | }
49 | return (
50 | <Stack direction="row" spacing={1}>
51 | <TextDimensionInput
52 | label="Column 1"
53 | defaultValue={currentValue?.[0]}
54 | onChange={(v) => {
55 | setIndexValue(0, v);
56 | }}
57 | />
58 | <TextDimensionInput
59 | label="Column 2"
60 | defaultValue={currentValue?.[1]}
61 | onChange={(v) => {
62 | setIndexValue(1, v);
63 | }}
64 | />
65 | {column3}
66 | </Stack>
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/FontFamily.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { MenuItem, TextField } from '@mui/material';
4 |
5 | import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
6 |
7 | const OPTIONS = FONT_FAMILIES.map((option) => (
8 | <MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
9 | {option.label}
10 | </MenuItem>
11 | ));
12 |
13 | type NullableProps = {
14 | label: string;
15 | onChange: (value: null | string) => void;
16 | defaultValue: null | string;
17 | };
18 | export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
19 | const [value, setValue] = useState(defaultValue ?? 'inherit');
20 | return (
21 | <TextField
22 | select
23 | variant="standard"
24 | label={label}
25 | value={value}
26 | onChange={(ev) => {
27 | const v = ev.target.value;
28 | setValue(v);
29 | onChange(v === null ? null : v);
30 | }}
31 | >
32 | <MenuItem value="inherit">Match email settings</MenuItem>
33 | {OPTIONS}
34 | </TextField>
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/FontSizeInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { TextFieldsOutlined } from '@mui/icons-material';
4 | import { InputLabel, Stack } from '@mui/material';
5 |
6 | import RawSliderInput from './raw/RawSliderInput';
7 |
8 | type Props = {
9 | label: string;
10 | defaultValue: number;
11 | onChange: (v: number) => void;
12 | };
13 | export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
14 | const [value, setValue] = useState(defaultValue);
15 | const handleChange = (value: number) => {
16 | setValue(value);
17 | onChange(value);
18 | };
19 | return (
20 | <Stack spacing={1} alignItems="flex-start">
21 | <InputLabel shrink>{label}</InputLabel>
22 | <RawSliderInput
23 | iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
24 | value={value}
25 | setValue={handleChange}
26 | units="px"
27 | step={1}
28 | min={10}
29 | max={48}
30 | />
31 | </Stack>
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/FontWeightInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { ToggleButton } from '@mui/material';
4 |
5 | import RadioGroupInput from './RadioGroupInput';
6 |
7 | type Props = {
8 | label: string;
9 | defaultValue: string;
10 | onChange: (value: string) => void;
11 | };
12 | export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
13 | const [value, setValue] = useState(defaultValue);
14 | return (
15 | <RadioGroupInput
16 | label={label}
17 | defaultValue={value}
18 | onChange={(fontWeight) => {
19 | setValue(fontWeight);
20 | onChange(fontWeight);
21 | }}
22 | >
23 | <ToggleButton value="normal">Regular</ToggleButton>
24 | <ToggleButton value="bold">Bold</ToggleButton>
25 | </RadioGroupInput>
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/PaddingInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {
4 | AlignHorizontalLeftOutlined,
5 | AlignHorizontalRightOutlined,
6 | AlignVerticalBottomOutlined,
7 | AlignVerticalTopOutlined,
8 | } from '@mui/icons-material';
9 | import { InputLabel, Stack } from '@mui/material';
10 |
11 | import RawSliderInput from './raw/RawSliderInput';
12 |
13 | type TPaddingValue = {
14 | top: number;
15 | bottom: number;
16 | right: number;
17 | left: number;
18 | };
19 | type Props = {
20 | label: string;
21 | defaultValue: TPaddingValue | null;
22 | onChange: (value: TPaddingValue) => void;
23 | };
24 | export default function PaddingInput({ label, defaultValue, onChange }: Props) {
25 | const [value, setValue] = useState(() => {
26 | if (defaultValue) {
27 | return defaultValue;
28 | }
29 | return {
30 | top: 0,
31 | left: 0,
32 | bottom: 0,
33 | right: 0,
34 | };
35 | });
36 |
37 | function handleChange(internalName: keyof TPaddingValue, nValue: number) {
38 | const v = {
39 | ...value,
40 | [internalName]: nValue,
41 | };
42 | setValue(v);
43 | onChange(v);
44 | }
45 |
46 | return (
47 | <Stack spacing={2} alignItems="flex-start" pb={1}>
48 | <InputLabel shrink>{label}</InputLabel>
49 |
50 | <RawSliderInput
51 | iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
52 | value={value.top}
53 | setValue={(num) => handleChange('top', num)}
54 | units="px"
55 | step={4}
56 | min={0}
57 | max={80}
58 | marks
59 | />
60 |
61 | <RawSliderInput
62 | iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
63 | value={value.bottom}
64 | setValue={(num) => handleChange('bottom', num)}
65 | units="px"
66 | step={4}
67 | min={0}
68 | max={80}
69 | marks
70 | />
71 |
72 | <RawSliderInput
73 | iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
74 | value={value.left}
75 | setValue={(num) => handleChange('left', num)}
76 | units="px"
77 | step={4}
78 | min={0}
79 | max={80}
80 | marks
81 | />
82 |
83 | <RawSliderInput
84 | iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
85 | value={value.right}
86 | setValue={(num) => handleChange('right', num)}
87 | units="px"
88 | step={4}
89 | min={0}
90 | max={80}
91 | marks
92 | />
93 | </Stack>
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/RadioGroupInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
4 |
5 | type Props = {
6 | label: string | JSX.Element;
7 | children: JSX.Element | JSX.Element[];
8 | defaultValue: string;
9 | onChange: (v: string) => void;
10 | };
11 | export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
12 | const [value, setValue] = useState(defaultValue);
13 | return (
14 | <Stack alignItems="flex-start">
15 | <InputLabel shrink>{label}</InputLabel>
16 | <ToggleButtonGroup
17 | exclusive
18 | fullWidth
19 | value={value}
20 | size="small"
21 | onChange={(_, v: unknown) => {
22 | if (typeof v !== 'string') {
23 | throw new Error('RadioGroupInput can only receive string values');
24 | }
25 | setValue(v);
26 | onChange(v);
27 | }}
28 | >
29 | {children}
30 | </ToggleButtonGroup>
31 | </Stack>
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/SliderInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { InputLabel, Stack } from '@mui/material';
4 |
5 | import RawSliderInput from './raw/RawSliderInput';
6 |
7 | type SliderInputProps = {
8 | label: string;
9 | iconLabel: JSX.Element;
10 |
11 | step?: number;
12 | marks?: boolean;
13 | units: string;
14 | min?: number;
15 | max?: number;
16 |
17 | defaultValue: number;
18 | onChange: (v: number) => void;
19 | };
20 |
21 | export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
22 | const [value, setValue] = useState(defaultValue);
23 | return (
24 | <Stack spacing={1} alignItems="flex-start">
25 | <InputLabel shrink>{label}</InputLabel>
26 | <RawSliderInput
27 | value={value}
28 | setValue={(value: number) => {
29 | setValue(value);
30 | onChange(value);
31 | }}
32 | {...props}
33 | />
34 | </Stack>
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/TextAlignInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
4 | import { ToggleButton } from '@mui/material';
5 |
6 | import RadioGroupInput from './RadioGroupInput';
7 |
8 | type Props = {
9 | label: string;
10 | defaultValue: string | null;
11 | onChange: (value: string | null) => void;
12 | };
13 | export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
14 | const [value, setValue] = useState(defaultValue ?? 'left');
15 |
16 | return (
17 | <RadioGroupInput
18 | label={label}
19 | defaultValue={value}
20 | onChange={(value) => {
21 | setValue(value);
22 | onChange(value);
23 | }}
24 | >
25 | <ToggleButton value="left">
26 | <FormatAlignLeftOutlined fontSize="small" />
27 | </ToggleButton>
28 | <ToggleButton value="center">
29 | <FormatAlignCenterOutlined fontSize="small" />
30 | </ToggleButton>
31 | <ToggleButton value="right">
32 | <FormatAlignRightOutlined fontSize="small" />
33 | </ToggleButton>
34 | </RadioGroupInput>
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/TextDimensionInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { TextField, Typography } from '@mui/material';
4 |
5 | type TextDimensionInputProps = {
6 | label: string;
7 | defaultValue: number | null | undefined;
8 | onChange: (v: number | null) => void;
9 | };
10 | export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
11 | const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
12 | const value = parseInt(ev.target.value);
13 | onChange(isNaN(value) ? null : value);
14 | };
15 | return (
16 | <TextField
17 | fullWidth
18 | onChange={handleChange}
19 | defaultValue={defaultValue}
20 | label={label}
21 | variant="standard"
22 | placeholder="auto"
23 | size="small"
24 | InputProps={{
25 | endAdornment: (
26 | <Typography variant="body2" color="text.secondary">
27 | px
28 | </Typography>
29 | ),
30 | }}
31 | />
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/TextInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { InputProps, TextField } from '@mui/material';
4 |
5 | type Props = {
6 | label: string;
7 | rows?: number;
8 | placeholder?: string;
9 | helperText?: string | JSX.Element;
10 | InputProps?: InputProps;
11 | defaultValue: string;
12 | onChange: (v: string) => void;
13 | };
14 | export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
15 | const [value, setValue] = useState(defaultValue);
16 | const isMultiline = typeof rows === 'number' && rows > 1;
17 | return (
18 | <TextField
19 | fullWidth
20 | multiline={isMultiline}
21 | minRows={rows}
22 | variant={isMultiline ? 'outlined' : 'standard'}
23 | label={label}
24 | placeholder={placeholder}
25 | helperText={helperText}
26 | InputProps={InputProps}
27 | value={value}
28 | onChange={(ev) => {
29 | const v = ev.target.value;
30 | setValue(v);
31 | onChange(v);
32 | }}
33 | />
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/inputs/raw/RawSliderInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Slider, Stack, Typography } from '@mui/material';
4 |
5 | type SliderInputProps = {
6 | iconLabel: JSX.Element;
7 |
8 | step?: number;
9 | marks?: boolean;
10 | units: string;
11 | min?: number;
12 | max?: number;
13 |
14 | value: number;
15 | setValue: (v: number) => void;
16 | };
17 |
18 | export default function RawSliderInput({ iconLabel, value, setValue, units, ...props }: SliderInputProps) {
19 | return (
20 | <Stack direction="row" alignItems="center" spacing={2} justifyContent="space-between" width="100%">
21 | <Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
22 | <Slider
23 | {...props}
24 | value={value}
25 | onChange={(_, value: unknown) => {
26 | if (typeof value !== 'number') {
27 | throw new Error('RawSliderInput values can only receive numeric values');
28 | }
29 | setValue(value);
30 | }}
31 | />
32 | <Box sx={{ minWidth: 32, textAlign: 'right', flexShrink: 0 }}>
33 | <Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1 }}>
34 | {value}
35 | {units}
36 | </Typography>
37 | </Box>
38 | </Stack>
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/style-inputs/MultiStylePropertyPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { TStyle } from '../../../../../../documents/blocks/helpers/TStyle';
4 |
5 | import SingleStylePropertyPanel from './SingleStylePropertyPanel';
6 |
7 | type MultiStylePropertyPanelProps = {
8 | names: (keyof TStyle)[];
9 | value: TStyle | undefined | null;
10 | onChange: (style: TStyle) => void;
11 | };
12 | export default function MultiStylePropertyPanel({ names, value, onChange }: MultiStylePropertyPanelProps) {
13 | return (
14 | <>
15 | {names.map((name) => (
16 | <SingleStylePropertyPanel key={name} name={name} value={value || {}} onChange={onChange} />
17 | ))}
18 | </>
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ConfigurationPanel/input-panels/helpers/style-inputs/SingleStylePropertyPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { RoundedCornerOutlined } from '@mui/icons-material';
4 |
5 | import { TStyle } from '../../../../../../documents/blocks/helpers/TStyle';
6 | import { NullableColorInput } from '../inputs/ColorInput';
7 | import { NullableFontFamily } from '../inputs/FontFamily';
8 | import FontSizeInput from '../inputs/FontSizeInput';
9 | import FontWeightInput from '../inputs/FontWeightInput';
10 | import PaddingInput from '../inputs/PaddingInput';
11 | import SliderInput from '../inputs/SliderInput';
12 | import TextAlignInput from '../inputs/TextAlignInput';
13 |
14 | type StylePropertyPanelProps = {
15 | name: keyof TStyle;
16 | value: TStyle;
17 | onChange: (style: TStyle) => void;
18 | };
19 | export default function SingleStylePropertyPanel({ name, value, onChange }: StylePropertyPanelProps) {
20 | const defaultValue = value[name] ?? null;
21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
22 | const handleChange = (v: any) => {
23 | onChange({ ...value, [name]: v });
24 | };
25 |
26 | switch (name) {
27 | case 'backgroundColor':
28 | return <NullableColorInput label="Background color" defaultValue={defaultValue} onChange={handleChange} />;
29 | case 'borderColor':
30 | return <NullableColorInput label="Border color" defaultValue={defaultValue} onChange={handleChange} />;
31 | case 'borderRadius':
32 | return (
33 | <SliderInput
34 | iconLabel={<RoundedCornerOutlined />}
35 | units="px"
36 | step={4}
37 | marks
38 | min={0}
39 | max={48}
40 | label="Border radius"
41 | defaultValue={defaultValue}
42 | onChange={handleChange}
43 | />
44 | );
45 | case 'color':
46 | return <NullableColorInput label="Text color" defaultValue={defaultValue} onChange={handleChange} />;
47 | case 'fontFamily':
48 | return <NullableFontFamily label="Font family" defaultValue={defaultValue} onChange={handleChange} />;
49 | case 'fontSize':
50 | return <FontSizeInput label="Font size" defaultValue={defaultValue} onChange={handleChange} />;
51 | case 'fontWeight':
52 | return <FontWeightInput label="Font weight" defaultValue={defaultValue} onChange={handleChange} />;
53 | case 'textAlign':
54 | return <TextAlignInput label="Alignment" defaultValue={defaultValue} onChange={handleChange} />;
55 | case 'padding':
56 | return <PaddingInput label="Padding" defaultValue={defaultValue} onChange={handleChange} />;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/StylesPanel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { setDocument, useDocument } from '../../documents/editor/EditorContext';
4 |
5 | import EmailLayoutSidebarPanel from './ConfigurationPanel/input-panels/EmailLayoutSidebarPanel';
6 |
7 | export default function StylesPanel() {
8 | const block = useDocument().root;
9 | if (!block) {
10 | return <p>Block not found</p>;
11 | }
12 |
13 | const { data, type } = block;
14 | if (type !== 'EmailLayout') {
15 | throw new Error('Expected "root" element to be of type EmailLayout');
16 | }
17 |
18 | return <EmailLayoutSidebarPanel key="root" data={data} setData={(data) => setDocument({ root: { type, data } })} />;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/ToggleInspectorPanelButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AppRegistrationOutlined, LastPageOutlined } from '@mui/icons-material';
4 | import { IconButton } from '@mui/material';
5 |
6 | import { toggleInspectorDrawerOpen, useInspectorDrawerOpen } from '../../documents/editor/EditorContext';
7 |
8 | export default function ToggleInspectorPanelButton() {
9 | const inspectorDrawerOpen = useInspectorDrawerOpen();
10 |
11 | const handleClick = () => {
12 | toggleInspectorDrawerOpen();
13 | };
14 | if (inspectorDrawerOpen) {
15 | return (
16 | <IconButton onClick={handleClick}>
17 | <LastPageOutlined fontSize="small" />
18 | </IconButton>
19 | );
20 | }
21 | return (
22 | <IconButton onClick={handleClick}>
23 | <AppRegistrationOutlined fontSize="small" />
24 | </IconButton>
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/InspectorDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Drawer, Tab, Tabs } from '@mui/material';
4 |
5 | import { setSidebarTab, useInspectorDrawerOpen, useSelectedSidebarTab } from '../../documents/editor/EditorContext';
6 |
7 | import ConfigurationPanel from './ConfigurationPanel';
8 | import StylesPanel from './StylesPanel';
9 |
10 | export const INSPECTOR_DRAWER_WIDTH = 320;
11 |
12 | export default function InspectorDrawer() {
13 | const selectedSidebarTab = useSelectedSidebarTab();
14 | const inspectorDrawerOpen = useInspectorDrawerOpen();
15 |
16 | const renderCurrentSidebarPanel = () => {
17 | switch (selectedSidebarTab) {
18 | case 'block-configuration':
19 | return <ConfigurationPanel />;
20 | case 'styles':
21 | return <StylesPanel />;
22 | }
23 | };
24 |
25 | return (
26 | <Drawer
27 | variant="persistent"
28 | anchor="right"
29 | open={inspectorDrawerOpen}
30 | sx={{
31 | width: inspectorDrawerOpen ? INSPECTOR_DRAWER_WIDTH : 0,
32 | }}
33 | >
34 | <Box sx={{ width: INSPECTOR_DRAWER_WIDTH, height: 49, borderBottom: 1, borderColor: 'divider' }}>
35 | <Box px={2}>
36 | <Tabs value={selectedSidebarTab} onChange={(_, v) => setSidebarTab(v)}>
37 | <Tab value="styles" label="Styles" />
38 | <Tab value="block-configuration" label="Inspect" />
39 | </Tabs>
40 | </Box>
41 | </Box>
42 | <Box sx={{ width: INSPECTOR_DRAWER_WIDTH, height: 'calc(100% - 49px)', overflow: 'auto' }}>
43 | {renderCurrentSidebarPanel()}
44 | </Box>
45 | </Drawer>
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/SamplesDrawer/SidebarButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button } from '@mui/material';
4 |
5 | import { resetDocument } from '../../documents/editor/EditorContext';
6 | import getConfiguration from '../../getConfiguration';
7 |
8 | export default function SidebarButton({ href, children }: { href: string; children: JSX.Element | string }) {
9 | const handleClick = () => {
10 | resetDocument(getConfiguration(href));
11 | };
12 | return (
13 | <Button size="small" href={href} onClick={handleClick}>
14 | {children}
15 | </Button>
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/SamplesDrawer/ToggleSamplesPanelButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FirstPageOutlined, MenuOutlined } from '@mui/icons-material';
4 | import { IconButton } from '@mui/material';
5 |
6 | import { toggleSamplesDrawerOpen, useSamplesDrawerOpen } from '../../documents/editor/EditorContext';
7 |
8 | function useIcon() {
9 | const samplesDrawerOpen = useSamplesDrawerOpen();
10 | if (samplesDrawerOpen) {
11 | return <FirstPageOutlined fontSize="small" />;
12 | }
13 | return <MenuOutlined fontSize="small" />;
14 | }
15 |
16 | export default function ToggleSamplesPanelButton() {
17 | const icon = useIcon();
18 | return <IconButton onClick={toggleSamplesDrawerOpen}>{icon}</IconButton>;
19 | }
20 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/SamplesDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Button, Divider, Drawer, Link, Stack, Typography } from '@mui/material';
4 |
5 | import { useSamplesDrawerOpen } from '../../documents/editor/EditorContext';
6 |
7 | import SidebarButton from './SidebarButton';
8 | import logo from './waypoint.svg';
9 |
10 | export const SAMPLES_DRAWER_WIDTH = 240;
11 |
12 | export default function SamplesDrawer() {
13 | const samplesDrawerOpen = useSamplesDrawerOpen();
14 |
15 | return (
16 | <Drawer
17 | variant="persistent"
18 | anchor="left"
19 | open={samplesDrawerOpen}
20 | sx={{
21 | width: samplesDrawerOpen ? SAMPLES_DRAWER_WIDTH : 0,
22 | }}
23 | >
24 | <Stack spacing={3} py={1} px={2} width={SAMPLES_DRAWER_WIDTH} justifyContent="space-between" height="100%">
25 | <Stack spacing={2} sx={{ '& .MuiButtonBase-root': { width: '100%', justifyContent: 'flex-start' } }}>
26 | <Typography variant="h6" component="h1" sx={{ p: 0.75 }}>
27 | EmailBuilder.js
28 | </Typography>
29 |
30 | <Stack alignItems="flex-start">
31 | <SidebarButton href="#">Empty</SidebarButton>
32 | <SidebarButton href="#sample/welcome">Welcome email</SidebarButton>
33 | <SidebarButton href="#sample/one-time-password">One-time passcode (OTP)</SidebarButton>
34 | <SidebarButton href="#sample/reset-password">Reset password</SidebarButton>
35 | <SidebarButton href="#sample/order-ecomerce">E-commerce receipt</SidebarButton>
36 | <SidebarButton href="#sample/subscription-receipt">Subscription receipt</SidebarButton>
37 | <SidebarButton href="#sample/reservation-reminder">Reservation reminder</SidebarButton>
38 | <SidebarButton href="#sample/post-metrics-report">Post metrics</SidebarButton>
39 | <SidebarButton href="#sample/respond-to-message">Respond to inquiry</SidebarButton>
40 | </Stack>
41 |
42 | <Divider />
43 |
44 | <Stack>
45 | <Button size="small" href="https://www.usewaypoint.com/open-source/emailbuilderjs" target="_blank">
46 | Learn more
47 | </Button>
48 | <Button size="small" href="https://github.com/usewaypoint/email-builder-js" target="_blank">
49 | View on GitHub
50 | </Button>
51 | </Stack>
52 | </Stack>
53 | <Stack spacing={2} px={0.75} py={3}>
54 | <Link href="https://usewaypoint.com?utm_source=emailbuilderjs" target="_blank" sx={{ lineHeight: 1 }}>
55 | <Box component="img" src={logo} width={32} />
56 | </Link>
57 | <Box>
58 | <Typography variant="overline" gutterBottom>
59 | Looking to send emails?
60 | </Typography>
61 | <Typography variant="body2" color="text.secondary" paragraph>
62 | Waypoint is an end-to-end email API with a 'pro' version of this template builder with dynamic
63 | variables, loops, conditionals, drag and drop, layouts, and more.
64 | </Typography>
65 | </Box>
66 | <Button
67 | variant="contained"
68 | color="primary"
69 | sx={{ justifyContent: 'center' }}
70 | href="https://usewaypoint.com?utm_source=emailbuilderjs"
71 | target="_blank"
72 | >
73 | Learn more
74 | </Button>
75 | </Stack>
76 | </Stack>
77 | </Drawer>
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/DownloadJson/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 |
3 | import { FileDownloadOutlined } from '@mui/icons-material';
4 | import { IconButton, Tooltip } from '@mui/material';
5 |
6 | import { useDocument } from '../../../documents/editor/EditorContext';
7 |
8 | export default function DownloadJson() {
9 | const doc = useDocument();
10 | const href = useMemo(() => {
11 | return `data:text/plain,${encodeURIComponent(JSON.stringify(doc, null, ' '))}`;
12 | }, [doc]);
13 | return (
14 | <Tooltip title="Download JSON file">
15 | <IconButton href={href} download="emailTemplate.json">
16 | <FileDownloadOutlined fontSize="small" />
17 | </IconButton>
18 | </Tooltip>
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/HtmlPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 |
3 | import { renderToStaticMarkup } from '@usewaypoint/email-builder';
4 |
5 | import { useDocument } from '../../documents/editor/EditorContext';
6 |
7 | import HighlightedCodePanel from './helper/HighlightedCodePanel';
8 |
9 | export default function HtmlPanel() {
10 | const document = useDocument();
11 | const code = useMemo(() => renderToStaticMarkup(document, { rootBlockId: 'root' }), [document]);
12 | return <HighlightedCodePanel type="html" value={code} />;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/ImportJson/ImportJsonDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {
4 | Alert,
5 | Button,
6 | Dialog,
7 | DialogActions,
8 | DialogContent,
9 | DialogTitle,
10 | Link,
11 | TextField,
12 | Typography,
13 | } from '@mui/material';
14 |
15 | import { resetDocument } from '../../../documents/editor/EditorContext';
16 |
17 | import validateJsonStringValue from './validateJsonStringValue';
18 |
19 | type ImportJsonDialogProps = {
20 | onClose: () => void;
21 | };
22 | export default function ImportJsonDialog({ onClose }: ImportJsonDialogProps) {
23 | const [value, setValue] = useState('');
24 | const [error, setError] = useState<string | null>(null);
25 |
26 | const handleChange: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (ev) => {
27 | const v = ev.currentTarget.value;
28 | setValue(v);
29 | const { error } = validateJsonStringValue(v);
30 | setError(error ?? null);
31 | };
32 |
33 | let errorAlert = null;
34 | if (error) {
35 | errorAlert = <Alert color="error">{error}</Alert>;
36 | }
37 |
38 | return (
39 | <Dialog open onClose={onClose}>
40 | <DialogTitle>Import JSON</DialogTitle>
41 | <form
42 | onSubmit={(ev) => {
43 | ev.preventDefault();
44 | const { error, data } = validateJsonStringValue(value);
45 | setError(error ?? null);
46 | if (!data) {
47 | return;
48 | }
49 | resetDocument(data);
50 | onClose();
51 | }}
52 | >
53 | <DialogContent>
54 | <Typography color="text.secondary" paragraph>
55 | Copy and paste an EmailBuilder.js JSON (
56 | <Link
57 | href="https://gist.githubusercontent.com/jordanisip/efb61f56ba71bd36d3a9440122cb7f50/raw/30ea74a6ac7e52ebdc309bce07b71a9286ce2526/emailBuilderTemplate.json"
58 | target="_blank"
59 | underline="none"
60 | >
61 | example
62 | </Link>
63 | ).
64 | </Typography>
65 | {errorAlert}
66 | <TextField
67 | error={error !== null}
68 | value={value}
69 | onChange={handleChange}
70 | type="text"
71 | helperText="This will override your current template."
72 | variant="outlined"
73 | fullWidth
74 | rows={10}
75 | multiline
76 | />
77 | </DialogContent>
78 | <DialogActions>
79 | <Button type="button" onClick={onClose}>
80 | Cancel
81 | </Button>
82 | <Button variant="contained" type="submit" disabled={error !== null}>
83 | Import
84 | </Button>
85 | </DialogActions>
86 | </form>
87 | </Dialog>
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/ImportJson/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { FileUploadOutlined } from '@mui/icons-material';
4 | import { IconButton, Tooltip } from '@mui/material';
5 |
6 | import ImportJsonDialog from './ImportJsonDialog';
7 |
8 | export default function ImportJson() {
9 | const [open, setOpen] = useState(false);
10 |
11 | let dialog = null;
12 | if (open) {
13 | dialog = <ImportJsonDialog onClose={() => setOpen(false)} />;
14 | }
15 |
16 | return (
17 | <>
18 | <Tooltip title="Import JSON">
19 | <IconButton onClick={() => setOpen(true)}>
20 | <FileUploadOutlined fontSize="small" />
21 | </IconButton>
22 | </Tooltip>
23 | {dialog}
24 | </>
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/ImportJson/validateJsonStringValue.ts:
--------------------------------------------------------------------------------
1 | import { EditorConfigurationSchema, TEditorConfiguration } from '../../../documents/editor/core';
2 |
3 | type TResult = { error: string; data?: undefined } | { data: TEditorConfiguration; error?: undefined };
4 |
5 | export default function validateTextAreaValue(value: string): TResult {
6 | let jsonObject = undefined;
7 | try {
8 | jsonObject = JSON.parse(value);
9 | } catch {
10 | return { error: 'Invalid json' };
11 | }
12 |
13 | const parseResult = EditorConfigurationSchema.safeParse(jsonObject);
14 | if (!parseResult.success) {
15 | return { error: 'Invalid JSON schema' };
16 | }
17 |
18 | if (!parseResult.data.root) {
19 | return { error: 'Missing "root" node' };
20 | }
21 |
22 | return { data: parseResult.data };
23 | }
24 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/JsonPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 |
3 | import { useDocument } from '../../documents/editor/EditorContext';
4 |
5 | import HighlightedCodePanel from './helper/HighlightedCodePanel';
6 |
7 | export default function JsonPanel() {
8 | const document = useDocument();
9 | const code = useMemo(() => JSON.stringify(document, null, ' '), [document]);
10 | return <HighlightedCodePanel type="json" value={code} />;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/MainTabsGroup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { CodeOutlined, DataObjectOutlined, EditOutlined, PreviewOutlined } from '@mui/icons-material';
4 | import { Tab, Tabs, Tooltip } from '@mui/material';
5 |
6 | import { setSelectedMainTab, useSelectedMainTab } from '../../documents/editor/EditorContext';
7 |
8 | export default function MainTabsGroup() {
9 | const selectedMainTab = useSelectedMainTab();
10 | const handleChange = (_: unknown, v: unknown) => {
11 | switch (v) {
12 | case 'json':
13 | case 'preview':
14 | case 'editor':
15 | case 'html':
16 | setSelectedMainTab(v);
17 | return;
18 | default:
19 | setSelectedMainTab('editor');
20 | }
21 | };
22 |
23 | return (
24 | <Tabs value={selectedMainTab} onChange={handleChange}>
25 | <Tab
26 | value="editor"
27 | label={
28 | <Tooltip title="Edit">
29 | <EditOutlined fontSize="small" />
30 | </Tooltip>
31 | }
32 | />
33 | <Tab
34 | value="preview"
35 | label={
36 | <Tooltip title="Preview">
37 | <PreviewOutlined fontSize="small" />
38 | </Tooltip>
39 | }
40 | />
41 | <Tab
42 | value="html"
43 | label={
44 | <Tooltip title="HTML output">
45 | <CodeOutlined fontSize="small" />
46 | </Tooltip>
47 | }
48 | />
49 | <Tab
50 | value="json"
51 | label={
52 | <Tooltip title="JSON output">
53 | <DataObjectOutlined fontSize="small" />
54 | </Tooltip>
55 | }
56 | />
57 | </Tabs>
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/ShareButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { IosShareOutlined } from '@mui/icons-material';
4 | import { IconButton, Snackbar, Tooltip } from '@mui/material';
5 |
6 | import { useDocument } from '../../documents/editor/EditorContext';
7 |
8 | export default function ShareButton() {
9 | const document = useDocument();
10 | const [message, setMessage] = useState<string | null>(null);
11 |
12 | const onClick = async () => {
13 | const c = encodeURIComponent(JSON.stringify(document));
14 | location.hash = `#code/${btoa(c)}`;
15 | setMessage('The URL was updated. Copy it to share your current template.');
16 | };
17 |
18 | const onClose = () => {
19 | setMessage(null);
20 | };
21 |
22 | return (
23 | <>
24 | <IconButton onClick={onClick}>
25 | <Tooltip title="Share current template">
26 | <IosShareOutlined fontSize="small" />
27 | </Tooltip>
28 | </IconButton>
29 | <Snackbar
30 | anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
31 | open={message !== null}
32 | onClose={onClose}
33 | message={message}
34 | />
35 | </>
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/helper/HighlightedCodePanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import { html, json } from './highlighters';
4 |
5 | type TextEditorPanelProps = {
6 | type: 'json' | 'html' | 'javascript';
7 | value: string;
8 | };
9 | export default function HighlightedCodePanel({ type, value }: TextEditorPanelProps) {
10 | const [code, setCode] = useState<string | null>(null);
11 |
12 | useEffect(() => {
13 | switch (type) {
14 | case 'html':
15 | html(value).then(setCode);
16 | return;
17 | case 'json':
18 | json(value).then(setCode);
19 | return;
20 | }
21 | }, [setCode, value, type]);
22 |
23 | if (code === null) {
24 | return null;
25 | }
26 |
27 | return (
28 | <pre
29 | style={{ margin: 0, padding: 16 }}
30 | dangerouslySetInnerHTML={{ __html: code }}
31 | onClick={(ev) => {
32 | const s = window.getSelection();
33 | if (s === null) {
34 | return;
35 | }
36 | s.selectAllChildren(ev.currentTarget);
37 | }}
38 | />
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/TemplatePanel/helper/highlighters.tsx:
--------------------------------------------------------------------------------
1 | import hljs from 'highlight.js';
2 | import jsonHighlighter from 'highlight.js/lib/languages/json';
3 | import xmlHighlighter from 'highlight.js/lib/languages/xml';
4 | import prettierPluginBabel from 'prettier/plugins/babel';
5 | import prettierPluginEstree from 'prettier/plugins/estree';
6 | import prettierPluginHtml from 'prettier/plugins/html';
7 | import { format } from 'prettier/standalone';
8 |
9 | hljs.registerLanguage('json', jsonHighlighter);
10 | hljs.registerLanguage('html', xmlHighlighter);
11 |
12 | export async function html(value: string): Promise<string> {
13 | const prettyValue = await format(value, {
14 | parser: 'html',
15 | plugins: [prettierPluginHtml],
16 | });
17 | return hljs.highlight(prettyValue, { language: 'html' }).value;
18 | }
19 |
20 | export async function json(value: string): Promise<string> {
21 | const prettyValue = await format(value, {
22 | parser: 'json',
23 | printWidth: 0,
24 | trailingComma: 'all',
25 | plugins: [prettierPluginBabel, prettierPluginEstree],
26 | });
27 | return hljs.highlight(prettyValue, { language: 'javascript' }).value;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/App/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Stack, useTheme } from '@mui/material';
4 |
5 | import { useInspectorDrawerOpen, useSamplesDrawerOpen } from '../documents/editor/EditorContext';
6 |
7 | import InspectorDrawer, { INSPECTOR_DRAWER_WIDTH } from './InspectorDrawer';
8 | import SamplesDrawer, { SAMPLES_DRAWER_WIDTH } from './SamplesDrawer';
9 | import TemplatePanel from './TemplatePanel';
10 |
11 | function useDrawerTransition(cssProperty: 'margin-left' | 'margin-right', open: boolean) {
12 | const { transitions } = useTheme();
13 | return transitions.create(cssProperty, {
14 | easing: !open ? transitions.easing.sharp : transitions.easing.easeOut,
15 | duration: !open ? transitions.duration.leavingScreen : transitions.duration.enteringScreen,
16 | });
17 | }
18 |
19 | export default function App() {
20 | const inspectorDrawerOpen = useInspectorDrawerOpen();
21 | const samplesDrawerOpen = useSamplesDrawerOpen();
22 |
23 | const marginLeftTransition = useDrawerTransition('margin-left', samplesDrawerOpen);
24 | const marginRightTransition = useDrawerTransition('margin-right', inspectorDrawerOpen);
25 |
26 | return (
27 | <>
28 | <InspectorDrawer />
29 | <SamplesDrawer />
30 |
31 | <Stack
32 | sx={{
33 | marginRight: inspectorDrawerOpen ? `${INSPECTOR_DRAWER_WIDTH}px` : 0,
34 | marginLeft: samplesDrawerOpen ? `${SAMPLES_DRAWER_WIDTH}px` : 0,
35 | transition: [marginLeftTransition, marginRightTransition].join(', '),
36 | }}
37 | >
38 | <TemplatePanel />
39 | </Stack>
40 | </>
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container';
4 |
5 | import { useCurrentBlockId } from '../../editor/EditorBlock';
6 | import { setDocument, setSelectedBlockId } from '../../editor/EditorContext';
7 | import EditorChildrenIds, { EditorChildrenChange } from '../helpers/EditorChildrenIds';
8 |
9 | import ColumnsContainerPropsSchema, { ColumnsContainerProps } from './ColumnsContainerPropsSchema';
10 |
11 | const EMPTY_COLUMNS = [{ childrenIds: [] }, { childrenIds: [] }, { childrenIds: [] }];
12 |
13 | export default function ColumnsContainerEditor({ style, props }: ColumnsContainerProps) {
14 | const currentBlockId = useCurrentBlockId();
15 |
16 | const { columns, ...restProps } = props ?? {};
17 | const columnsValue = columns ?? EMPTY_COLUMNS;
18 |
19 | const updateColumn = (columnIndex: 0 | 1 | 2, { block, blockId, childrenIds }: EditorChildrenChange) => {
20 | const nColumns = [...columnsValue];
21 | nColumns[columnIndex] = { childrenIds };
22 | setDocument({
23 | [blockId]: block,
24 | [currentBlockId]: {
25 | type: 'ColumnsContainer',
26 | data: ColumnsContainerPropsSchema.parse({
27 | style,
28 | props: {
29 | ...restProps,
30 | columns: nColumns,
31 | },
32 | }),
33 | },
34 | });
35 | setSelectedBlockId(blockId);
36 | };
37 |
38 | return (
39 | <BaseColumnsContainer
40 | props={restProps}
41 | style={style}
42 | columns={[
43 | <EditorChildrenIds childrenIds={columns?.[0]?.childrenIds} onChange={(change) => updateColumn(0, change)} />,
44 | <EditorChildrenIds childrenIds={columns?.[1]?.childrenIds} onChange={(change) => updateColumn(1, change)} />,
45 | <EditorChildrenIds childrenIds={columns?.[2]?.childrenIds} onChange={(change) => updateColumn(2, change)} />,
46 | ]}
47 | />
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container';
4 |
5 | const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape;
6 |
7 | const ColumnsContainerPropsSchema = z.object({
8 | style: BaseColumnsContainerPropsSchema.shape.style,
9 | props: z
10 | .object({
11 | ...BasePropsShape,
12 | columns: z.tuple([
13 | z.object({ childrenIds: z.array(z.string()) }),
14 | z.object({ childrenIds: z.array(z.string()) }),
15 | z.object({ childrenIds: z.array(z.string()) }),
16 | ]),
17 | })
18 | .optional()
19 | .nullable(),
20 | });
21 |
22 | export type ColumnsContainerProps = z.infer<typeof ColumnsContainerPropsSchema>;
23 | export default ColumnsContainerPropsSchema;
24 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/Container/ContainerEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container as BaseContainer } from '@usewaypoint/block-container';
4 |
5 | import { useCurrentBlockId } from '../../editor/EditorBlock';
6 | import { setDocument, setSelectedBlockId, useDocument } from '../../editor/EditorContext';
7 | import EditorChildrenIds from '../helpers/EditorChildrenIds';
8 |
9 | import { ContainerProps } from './ContainerPropsSchema';
10 |
11 | export default function ContainerEditor({ style, props }: ContainerProps) {
12 | const childrenIds = props?.childrenIds ?? [];
13 |
14 | const document = useDocument();
15 | const currentBlockId = useCurrentBlockId();
16 |
17 | return (
18 | <BaseContainer style={style}>
19 | <EditorChildrenIds
20 | childrenIds={childrenIds}
21 | onChange={({ block, blockId, childrenIds }) => {
22 | setDocument({
23 | [blockId]: block,
24 | [currentBlockId]: {
25 | type: 'Container',
26 | data: {
27 | ...document[currentBlockId].data,
28 | props: { childrenIds: childrenIds },
29 | },
30 | },
31 | });
32 | setSelectedBlockId(blockId);
33 | }}
34 | />
35 | </BaseContainer>
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/Container/ContainerPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container';
4 |
5 | const ContainerPropsSchema = z.object({
6 | style: BaseContainerPropsSchema.shape.style,
7 | props: z
8 | .object({
9 | childrenIds: z.array(z.string()).optional().nullable(),
10 | })
11 | .optional()
12 | .nullable(),
13 | });
14 |
15 | export default ContainerPropsSchema;
16 |
17 | export type ContainerProps = z.infer<typeof ContainerPropsSchema>;
18 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/EmailLayout/EmailLayoutPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const COLOR_SCHEMA = z
4 | .string()
5 | .regex(/^#[0-9a-fA-F]{6}$/)
6 | .nullable()
7 | .optional();
8 |
9 | const FONT_FAMILY_SCHEMA = z
10 | .enum([
11 | 'MODERN_SANS',
12 | 'BOOK_SANS',
13 | 'ORGANIC_SANS',
14 | 'GEOMETRIC_SANS',
15 | 'HEAVY_SANS',
16 | 'ROUNDED_SANS',
17 | 'MODERN_SERIF',
18 | 'BOOK_SERIF',
19 | 'MONOSPACE',
20 | ])
21 | .nullable()
22 | .optional();
23 |
24 | const EmailLayoutPropsSchema = z.object({
25 | backdropColor: COLOR_SCHEMA,
26 | borderColor: COLOR_SCHEMA,
27 | borderRadius: z.number().optional().nullable(),
28 | canvasColor: COLOR_SCHEMA,
29 | textColor: COLOR_SCHEMA,
30 | fontFamily: FONT_FAMILY_SCHEMA,
31 | childrenIds: z.array(z.string()).optional().nullable(),
32 | });
33 |
34 | export default EmailLayoutPropsSchema;
35 |
36 | export type EmailLayoutProps = z.infer<typeof EmailLayoutPropsSchema>;
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/BlockButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Button, SxProps, Typography } from '@mui/material';
4 |
5 | type BlockMenuButtonProps = {
6 | label: string;
7 | icon: React.ReactNode;
8 | onClick: () => void;
9 | };
10 |
11 | const BUTTON_SX: SxProps = { p: 1.5, display: 'flex', flexDirection: 'column' };
12 | const ICON_SX: SxProps = {
13 | mb: 0.75,
14 | width: '100%',
15 | bgcolor: 'cadet.200',
16 | display: 'flex',
17 | justifyContent: 'center',
18 | p: 1,
19 | border: '1px solid',
20 | borderColor: 'cadet.300',
21 | };
22 |
23 | export default function BlockTypeButton({ label, icon, onClick }: BlockMenuButtonProps) {
24 | return (
25 | <Button
26 | sx={BUTTON_SX}
27 | onClick={(ev) => {
28 | ev.stopPropagation();
29 | onClick();
30 | }}
31 | >
32 | <Box sx={ICON_SX}>{icon}</Box>
33 | <Typography variant="body2">{label}</Typography>
34 | </Button>
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/BlocksMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Menu } from '@mui/material';
4 |
5 | import { TEditorBlock } from '../../../../editor/core';
6 |
7 | import BlockButton from './BlockButton';
8 | import { BUTTONS } from './buttons';
9 |
10 | type BlocksMenuProps = {
11 | anchorEl: HTMLElement | null;
12 | setAnchorEl: (v: HTMLElement | null) => void;
13 | onSelect: (block: TEditorBlock) => void;
14 | };
15 | export default function BlocksMenu({ anchorEl, setAnchorEl, onSelect }: BlocksMenuProps) {
16 | const onClose = () => {
17 | setAnchorEl(null);
18 | };
19 |
20 | const onClick = (block: TEditorBlock) => {
21 | onSelect(block);
22 | setAnchorEl(null);
23 | };
24 |
25 | if (anchorEl === null) {
26 | return null;
27 | }
28 |
29 | return (
30 | <Menu
31 | open
32 | anchorEl={anchorEl}
33 | onClose={onClose}
34 | anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
35 | transformOrigin={{ vertical: 'top', horizontal: 'center' }}
36 | >
37 | <Box sx={{ p: 1, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr' }}>
38 | {BUTTONS.map((k, i) => (
39 | <BlockButton key={i} label={k.label} icon={k.icon} onClick={() => onClick(k.block())} />
40 | ))}
41 | </Box>
42 | </Menu>
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/DividerButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | import { AddOutlined } from '@mui/icons-material';
4 | import { Fade, IconButton } from '@mui/material';
5 |
6 | type Props = {
7 | buttonElement: HTMLElement | null;
8 | onClick: () => void;
9 | };
10 | export default function DividerButton({ buttonElement, onClick }: Props) {
11 | const [visible, setVisible] = useState(false);
12 |
13 | useEffect(() => {
14 | function listener({ clientX, clientY }: MouseEvent) {
15 | if (!buttonElement) {
16 | return;
17 | }
18 | const rect = buttonElement.getBoundingClientRect();
19 | const rectY = rect.y;
20 | const bottomX = rect.x;
21 | const topX = bottomX + rect.width;
22 |
23 | if (Math.abs(clientY - rectY) < 20) {
24 | if (bottomX < clientX && clientX < topX) {
25 | setVisible(true);
26 | return;
27 | }
28 | }
29 | setVisible(false);
30 | }
31 | window.addEventListener('mousemove', listener);
32 | return () => {
33 | window.removeEventListener('mousemove', listener);
34 | };
35 | }, [buttonElement, setVisible]);
36 |
37 | return (
38 | <Fade in={visible}>
39 | <IconButton
40 | size="small"
41 | sx={{
42 | p: 0.12,
43 | position: 'absolute',
44 | top: '-12px',
45 | left: '50%',
46 | transform: 'translateX(-10px)',
47 | bgcolor: 'brand.blue',
48 | color: 'primary.contrastText',
49 | zIndex: 'fab',
50 | '&:hover, &:active, &:focus': {
51 | bgcolor: 'brand.blue',
52 | color: 'primary.contrastText',
53 | },
54 | }}
55 | onClick={(ev) => {
56 | ev.stopPropagation();
57 | onClick();
58 | }}
59 | >
60 | <AddOutlined fontSize="small" />
61 | </IconButton>
62 | </Fade>
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/PlaceholderButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AddOutlined } from '@mui/icons-material';
4 | import { ButtonBase } from '@mui/material';
5 |
6 | type Props = {
7 | onClick: () => void;
8 | };
9 | export default function PlaceholderButton({ onClick }: Props) {
10 | return (
11 | <ButtonBase
12 | onClick={(ev) => {
13 | ev.stopPropagation();
14 | onClick();
15 | }}
16 | sx={{
17 | display: 'flex',
18 | alignContent: 'center',
19 | justifyContent: 'center',
20 | height: 48,
21 | width: '100%',
22 | bgcolor: 'rgba(0,0,0, 0.05)',
23 | }}
24 | >
25 | <AddOutlined
26 | sx={{
27 | p: 0.12,
28 | bgcolor: 'brand.blue',
29 | borderRadius: 24,
30 | color: 'primary.contrastText',
31 | }}
32 | fontSize="small"
33 | />
34 | </ButtonBase>
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/AddBlockMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { TEditorBlock } from '../../../../editor/core';
4 |
5 | import BlocksMenu from './BlocksMenu';
6 | import DividerButton from './DividerButton';
7 | import PlaceholderButton from './PlaceholderButton';
8 |
9 | type Props = {
10 | placeholder?: boolean;
11 | onSelect: (block: TEditorBlock) => void;
12 | };
13 | export default function AddBlockButton({ onSelect, placeholder }: Props) {
14 | const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
15 | const [buttonElement, setButtonElement] = useState<HTMLElement | null>(null);
16 |
17 | const handleButtonClick = () => {
18 | setMenuAnchorEl(buttonElement);
19 | };
20 |
21 | const renderButton = () => {
22 | if (placeholder) {
23 | return <PlaceholderButton onClick={handleButtonClick} />;
24 | } else {
25 | return <DividerButton buttonElement={buttonElement} onClick={handleButtonClick} />;
26 | }
27 | };
28 |
29 | return (
30 | <>
31 | <div ref={setButtonElement} style={{ position: 'relative' }}>
32 | {renderButton()}
33 | </div>
34 | <BlocksMenu anchorEl={menuAnchorEl} setAnchorEl={setMenuAnchorEl} onSelect={onSelect} />
35 | </>
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/EditorChildrenIds/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 |
3 | import { TEditorBlock } from '../../../editor/core';
4 | import EditorBlock from '../../../editor/EditorBlock';
5 |
6 | import AddBlockButton from './AddBlockMenu';
7 |
8 | export type EditorChildrenChange = {
9 | blockId: string;
10 | block: TEditorBlock;
11 | childrenIds: string[];
12 | };
13 |
14 | function generateId() {
15 | return `block-${Date.now()}`;
16 | }
17 |
18 | export type EditorChildrenIdsProps = {
19 | childrenIds: string[] | null | undefined;
20 | onChange: (val: EditorChildrenChange) => void;
21 | };
22 | export default function EditorChildrenIds({ childrenIds, onChange }: EditorChildrenIdsProps) {
23 | const appendBlock = (block: TEditorBlock) => {
24 | const blockId = generateId();
25 | return onChange({
26 | blockId,
27 | block,
28 | childrenIds: [...(childrenIds || []), blockId],
29 | });
30 | };
31 |
32 | const insertBlock = (block: TEditorBlock, index: number) => {
33 | const blockId = generateId();
34 | const newChildrenIds = [...(childrenIds || [])];
35 | newChildrenIds.splice(index, 0, blockId);
36 | return onChange({
37 | blockId,
38 | block,
39 | childrenIds: newChildrenIds,
40 | });
41 | };
42 |
43 | if (!childrenIds || childrenIds.length === 0) {
44 | return <AddBlockButton placeholder onSelect={appendBlock} />;
45 | }
46 |
47 | return (
48 | <>
49 | {childrenIds.map((childId, i) => (
50 | <Fragment key={childId}>
51 | <AddBlockButton onSelect={(block) => insertBlock(block, i)} />
52 | <EditorBlock id={childId} />
53 | </Fragment>
54 | ))}
55 | <AddBlockButton onSelect={appendBlock} />
56 | </>
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/TStyle.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | export type TStyle = {
4 | backgroundColor?: any;
5 | borderColor?: any;
6 | borderRadius?: any;
7 | color?: any;
8 | fontFamily?: any;
9 | fontSize?: any;
10 | fontWeight?: any;
11 | padding?: any;
12 | textAlign?: any;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/EditorBlockWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useState } from 'react';
2 |
3 | import { Box } from '@mui/material';
4 |
5 | import { useCurrentBlockId } from '../../../editor/EditorBlock';
6 | import { setSelectedBlockId, useSelectedBlockId } from '../../../editor/EditorContext';
7 |
8 | import TuneMenu from './TuneMenu';
9 |
10 | type TEditorBlockWrapperProps = {
11 | children: JSX.Element;
12 | };
13 |
14 | export default function EditorBlockWrapper({ children }: TEditorBlockWrapperProps) {
15 | const selectedBlockId = useSelectedBlockId();
16 | const [mouseInside, setMouseInside] = useState(false);
17 | const blockId = useCurrentBlockId();
18 |
19 | let outline: CSSProperties['outline'];
20 | if (selectedBlockId === blockId) {
21 | outline = '2px solid rgba(0,121,204, 1)';
22 | } else if (mouseInside) {
23 | outline = '2px solid rgba(0,121,204, 0.3)';
24 | }
25 |
26 | const renderMenu = () => {
27 | if (selectedBlockId !== blockId) {
28 | return null;
29 | }
30 | return <TuneMenu blockId={blockId} />;
31 | };
32 |
33 | return (
34 | <Box
35 | sx={{
36 | position: 'relative',
37 | maxWidth: '100%',
38 | outlineOffset: '-1px',
39 | outline,
40 | }}
41 | onMouseEnter={(ev) => {
42 | setMouseInside(true);
43 | ev.stopPropagation();
44 | }}
45 | onMouseLeave={() => {
46 | setMouseInside(false);
47 | }}
48 | onClick={(ev) => {
49 | setSelectedBlockId(blockId);
50 | ev.stopPropagation();
51 | ev.preventDefault();
52 | }}
53 | >
54 | {renderMenu()}
55 | {children}
56 | </Box>
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/block-wrappers/ReaderBlockWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 |
3 | import { TStyle } from '../TStyle';
4 |
5 | type TReaderBlockWrapperProps = {
6 | style: TStyle;
7 | children: JSX.Element;
8 | };
9 |
10 | export default function ReaderBlockWrapper({ style, children }: TReaderBlockWrapperProps) {
11 | const { padding, borderColor, ...restStyle } = style;
12 | const cssStyle: CSSProperties = {
13 | ...restStyle,
14 | };
15 |
16 | if (padding) {
17 | const { top, bottom, left, right } = padding;
18 | cssStyle.padding = `${top}px ${right}px ${bottom}px ${left}px`;
19 | }
20 |
21 | if (borderColor) {
22 | cssStyle.border = `1px solid ${borderColor}`;
23 | }
24 |
25 | return <div style={{ maxWidth: '100%', ...cssStyle }}>{children}</div>;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/fontFamily.ts:
--------------------------------------------------------------------------------
1 | export const FONT_FAMILIES = [
2 | {
3 | key: 'MODERN_SANS',
4 | label: 'Modern sans',
5 | value: '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif',
6 | },
7 | {
8 | key: 'BOOK_SANS',
9 | label: 'Book sans',
10 | value: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif',
11 | },
12 | {
13 | key: 'ORGANIC_SANS',
14 | label: 'Organic sans',
15 | value: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif',
16 | },
17 | {
18 | key: 'GEOMETRIC_SANS',
19 | label: 'Geometric sans',
20 | value: 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif',
21 | },
22 | {
23 | key: 'HEAVY_SANS',
24 | label: 'Heavy sans',
25 | value:
26 | 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif',
27 | },
28 | {
29 | key: 'ROUNDED_SANS',
30 | label: 'Rounded sans',
31 | value:
32 | 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif',
33 | },
34 | {
35 | key: 'MODERN_SERIF',
36 | label: 'Modern serif',
37 | value: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif',
38 | },
39 | {
40 | key: 'BOOK_SERIF',
41 | label: 'Book serif',
42 | value: '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif',
43 | },
44 | {
45 | key: 'MONOSPACE',
46 | label: 'Monospace',
47 | value: '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace',
48 | },
49 | ];
50 |
51 | export const FONT_FAMILY_NAMES = [
52 | 'MODERN_SANS',
53 | 'BOOK_SANS',
54 | 'ORGANIC_SANS',
55 | 'GEOMETRIC_SANS',
56 | 'HEAVY_SANS',
57 | 'ROUNDED_SANS',
58 | 'MODERN_SERIF',
59 | 'BOOK_SERIF',
60 | 'MONOSPACE',
61 | ] as const;
62 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/blocks/helpers/zod.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { FONT_FAMILY_NAMES } from './fontFamily';
4 |
5 | export function zColor() {
6 | return z.string().regex(/^#[0-9a-fA-F]{6}$/);
7 | }
8 |
9 | export function zFontFamily() {
10 | return z.enum(FONT_FAMILY_NAMES);
11 | }
12 |
13 | export function zFontWeight() {
14 | return z.enum(['bold', 'normal']);
15 | }
16 |
17 | export function zTextAlign() {
18 | return z.enum(['left', 'center', 'right']);
19 | }
20 |
21 | export function zPadding() {
22 | return z.object({
23 | top: z.number(),
24 | bottom: z.number(),
25 | right: z.number(),
26 | left: z.number(),
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/editor/EditorBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from 'react';
2 |
3 | import { EditorBlock as CoreEditorBlock } from './core';
4 | import { useDocument } from './EditorContext';
5 |
6 | const EditorBlockContext = createContext<string | null>(null);
7 | export const useCurrentBlockId = () => useContext(EditorBlockContext)!;
8 |
9 | type EditorBlockProps = {
10 | id: string;
11 | };
12 |
13 | /**
14 | *
15 | * @param id - Block id
16 | * @returns EditorBlock component that loads data from the EditorDocumentContext
17 | */
18 | export default function EditorBlock({ id }: EditorBlockProps) {
19 | const document = useDocument();
20 | const block = document[id];
21 | if (!block) {
22 | throw new Error('Could not find block');
23 | }
24 | return (
25 | <EditorBlockContext.Provider value={id}>
26 | <CoreEditorBlock {...block} />
27 | </EditorBlockContext.Provider>
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/documents/editor/EditorContext.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | import getConfiguration from '../../getConfiguration';
4 |
5 | import { TEditorConfiguration } from './core';
6 |
7 | type TValue = {
8 | document: TEditorConfiguration;
9 |
10 | selectedBlockId: string | null;
11 | selectedSidebarTab: 'block-configuration' | 'styles';
12 | selectedMainTab: 'editor' | 'preview' | 'json' | 'html';
13 | selectedScreenSize: 'desktop' | 'mobile';
14 |
15 | inspectorDrawerOpen: boolean;
16 | samplesDrawerOpen: boolean;
17 | };
18 |
19 | const editorStateStore = create<TValue>(() => ({
20 | document: getConfiguration(window.location.hash),
21 | selectedBlockId: null,
22 | selectedSidebarTab: 'styles',
23 | selectedMainTab: 'editor',
24 | selectedScreenSize: 'desktop',
25 |
26 | inspectorDrawerOpen: true,
27 | samplesDrawerOpen: true,
28 | }));
29 |
30 | export function useDocument() {
31 | return editorStateStore((s) => s.document);
32 | }
33 |
34 | export function useSelectedBlockId() {
35 | return editorStateStore((s) => s.selectedBlockId);
36 | }
37 |
38 | export function useSelectedScreenSize() {
39 | return editorStateStore((s) => s.selectedScreenSize);
40 | }
41 |
42 | export function useSelectedMainTab() {
43 | return editorStateStore((s) => s.selectedMainTab);
44 | }
45 |
46 | export function setSelectedMainTab(selectedMainTab: TValue['selectedMainTab']) {
47 | return editorStateStore.setState({ selectedMainTab });
48 | }
49 |
50 | export function useSelectedSidebarTab() {
51 | return editorStateStore((s) => s.selectedSidebarTab);
52 | }
53 |
54 | export function useInspectorDrawerOpen() {
55 | return editorStateStore((s) => s.inspectorDrawerOpen);
56 | }
57 |
58 | export function useSamplesDrawerOpen() {
59 | return editorStateStore((s) => s.samplesDrawerOpen);
60 | }
61 |
62 | export function setSelectedBlockId(selectedBlockId: TValue['selectedBlockId']) {
63 | const selectedSidebarTab = selectedBlockId === null ? 'styles' : 'block-configuration';
64 | const options: Partial<TValue> = {};
65 | if (selectedBlockId !== null) {
66 | options.inspectorDrawerOpen = true;
67 | }
68 | return editorStateStore.setState({
69 | selectedBlockId,
70 | selectedSidebarTab,
71 | ...options,
72 | });
73 | }
74 |
75 | export function setSidebarTab(selectedSidebarTab: TValue['selectedSidebarTab']) {
76 | return editorStateStore.setState({ selectedSidebarTab });
77 | }
78 |
79 | export function resetDocument(document: TValue['document']) {
80 | return editorStateStore.setState({
81 | document,
82 | selectedSidebarTab: 'styles',
83 | selectedBlockId: null,
84 | });
85 | }
86 |
87 | export function setDocument(document: TValue['document']) {
88 | const originalDocument = editorStateStore.getState().document;
89 | return editorStateStore.setState({
90 | document: {
91 | ...originalDocument,
92 | ...document,
93 | },
94 | });
95 | }
96 |
97 | export function toggleInspectorDrawerOpen() {
98 | const inspectorDrawerOpen = !editorStateStore.getState().inspectorDrawerOpen;
99 | return editorStateStore.setState({ inspectorDrawerOpen });
100 | }
101 |
102 | export function toggleSamplesDrawerOpen() {
103 | const samplesDrawerOpen = !editorStateStore.getState().samplesDrawerOpen;
104 | return editorStateStore.setState({ samplesDrawerOpen });
105 | }
106 |
107 | export function setSelectedScreenSize(selectedScreenSize: TValue['selectedScreenSize']) {
108 | return editorStateStore.setState({ selectedScreenSize });
109 | }
110 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/packages/editor-sample/src/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usewaypoint/email-builder-js/1ded24dca92f90488c27f9ef536d37a2d6692ce5/packages/editor-sample/src/favicon/favicon.ico
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/index.tsx:
--------------------------------------------------------------------------------
1 | import EMPTY_EMAIL_MESSAGE from './sample/empty-email-message';
2 | import ONE_TIME_PASSCODE from './sample/one-time-passcode';
3 | import ORDER_ECOMMERCE from './sample/order-ecommerce';
4 | import POST_METRICS_REPORT from './sample/post-metrics-report';
5 | import RESERVATION_REMINDER from './sample/reservation-reminder';
6 | import RESET_PASSWORD from './sample/reset-password';
7 | import RESPOND_TO_MESSAGE from './sample/respond-to-message';
8 | import SUBSCRIPTION_RECEIPT from './sample/subscription-receipt';
9 | import WELCOME from './sample/welcome';
10 |
11 | export default function getConfiguration(template: string) {
12 | if (template.startsWith('#sample/')) {
13 | const sampleName = template.replace('#sample/', '');
14 | switch (sampleName) {
15 | case 'welcome':
16 | return WELCOME;
17 | case 'one-time-password':
18 | return ONE_TIME_PASSCODE;
19 | case 'order-ecomerce':
20 | return ORDER_ECOMMERCE;
21 | case 'post-metrics-report':
22 | return POST_METRICS_REPORT;
23 | case 'reservation-reminder':
24 | return RESERVATION_REMINDER;
25 | case 'reset-password':
26 | return RESET_PASSWORD;
27 | case 'respond-to-message':
28 | return RESPOND_TO_MESSAGE;
29 | case 'subscription-receipt':
30 | return SUBSCRIPTION_RECEIPT;
31 | }
32 | }
33 |
34 | if (template.startsWith('#code/')) {
35 | const encodedString = template.replace('#code/', '');
36 | const configurationString = decodeURIComponent(atob(encodedString));
37 | try {
38 | return JSON.parse(configurationString);
39 | } catch {
40 | console.error(`Couldn't load configuration from hash.`);
41 | }
42 | }
43 |
44 | return EMPTY_EMAIL_MESSAGE;
45 | }
46 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/sample/empty-email-message.ts:
--------------------------------------------------------------------------------
1 | import { TEditorConfiguration } from '../../documents/editor/core';
2 |
3 | const EMPTY_EMAIL_MESSAGE: TEditorConfiguration = {
4 | root: {
5 | type: 'EmailLayout',
6 | data: {
7 | backdropColor: '#F5F5F5',
8 | canvasColor: '#FFFFFF',
9 | textColor: '#262626',
10 | fontFamily: 'MODERN_SANS',
11 | childrenIds: [],
12 | },
13 | },
14 | };
15 |
16 | export default EMPTY_EMAIL_MESSAGE;
17 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/getConfiguration/sample/one-time-passcode.ts:
--------------------------------------------------------------------------------
1 | import { TEditorConfiguration } from '../../documents/editor/core';
2 |
3 | const ONE_TIME_PASSCODE: TEditorConfiguration = {
4 | root: {
5 | type: 'EmailLayout',
6 | data: {
7 | backdropColor: '#000000',
8 | canvasColor: '#000000',
9 | textColor: '#FFFFFF',
10 | fontFamily: 'BOOK_SERIF',
11 | childrenIds: [
12 | 'block_ChPX66qUhF46uynDE8AY11',
13 | 'block_CkNrtQgkqPt2YWLv1hr5eJ',
14 | 'block_BFLBa3q5y8kax9KngyXP65',
15 | 'block_4T7sDFb4rqbSyWjLGJKmov',
16 | 'block_Rvc8ZfTjfhXjpphHquJKvP',
17 | ],
18 | },
19 | },
20 | block_ChPX66qUhF46uynDE8AY11: {
21 | type: 'Image',
22 | data: {
23 | style: {
24 | backgroundColor: null,
25 | padding: {
26 | top: 24,
27 | bottom: 24,
28 | left: 24,
29 | right: 24,
30 | },
31 | textAlign: 'center',
32 | },
33 | props: {
34 | height: 24,
35 | url: 'https://d1iiu589g39o6c.cloudfront.net/live/platforms/platform_A9wwKSL6EV6orh6f/images/wptemplateimage_jc7ZfPvdHJ6rtH1W/&.png',
36 | contentAlignment: 'middle',
37 | },
38 | },
39 | },
40 | block_CkNrtQgkqPt2YWLv1hr5eJ: {
41 | type: 'Text',
42 | data: {
43 | style: {
44 | color: '#ffffff',
45 | backgroundColor: null,
46 | fontSize: 16,
47 | fontFamily: null,
48 | fontWeight: 'normal',
49 | textAlign: 'center',
50 | padding: {
51 | top: 16,
52 | bottom: 16,
53 | left: 24,
54 | right: 24,
55 | },
56 | },
57 | props: {
58 | text: 'Here is your one-time passcode:',
59 | },
60 | },
61 | },
62 | block_BFLBa3q5y8kax9KngyXP65: {
63 | type: 'Heading',
64 | data: {
65 | style: {
66 | color: null,
67 | backgroundColor: null,
68 | fontFamily: 'MONOSPACE',
69 | fontWeight: 'bold',
70 | textAlign: 'center',
71 | padding: {
72 | top: 16,
73 | bottom: 16,
74 | left: 24,
75 | right: 24,
76 | },
77 | },
78 | props: {
79 | level: 'h1',
80 | text: '0123456',
81 | },
82 | },
83 | },
84 | block_4T7sDFb4rqbSyWjLGJKmov: {
85 | type: 'Text',
86 | data: {
87 | style: {
88 | color: '#868686',
89 | backgroundColor: null,
90 | fontSize: 16,
91 | fontFamily: null,
92 | fontWeight: 'normal',
93 | textAlign: 'center',
94 | padding: {
95 | top: 16,
96 | bottom: 16,
97 | left: 24,
98 | right: 24,
99 | },
100 | },
101 | props: {
102 | text: 'This code will expire in 30 minutes.',
103 | },
104 | },
105 | },
106 | block_Rvc8ZfTjfhXjpphHquJKvP: {
107 | type: 'Text',
108 | data: {
109 | style: {
110 | color: '#868686',
111 | backgroundColor: null,
112 | fontSize: 14,
113 | fontFamily: null,
114 | fontWeight: 'normal',
115 | textAlign: 'center',
116 | padding: {
117 | top: 16,
118 | bottom: 16,
119 | left: 24,
120 | right: 24,
121 | },
122 | },
123 | props: {
124 | text: 'Problems? Just reply to this email.',
125 | },
126 | },
127 | },
128 | };
129 |
130 | export default ONE_TIME_PASSCODE;
131 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import { CssBaseline, ThemeProvider } from '@mui/material';
5 |
6 | import App from './App';
7 | import theme from './theme';
8 |
9 | ReactDOM.createRoot(document.getElementById('root')!).render(
10 | <React.StrictMode>
11 | <ThemeProvider theme={theme}>
12 | <CssBaseline />
13 | <App />
14 | </ThemeProvider>
15 | </React.StrictMode>
16 | );
17 |
--------------------------------------------------------------------------------
/packages/editor-sample/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="vite/client" />
2 |
--------------------------------------------------------------------------------
/packages/editor-sample/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/editor-sample/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | import react from '@vitejs/plugin-react-swc';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | base: '/email-builder-js/',
8 | });
9 |
--------------------------------------------------------------------------------
/packages/email-builder/.npmignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .envrc
3 | .eslintignore
4 | .eslintrc.json
5 | .prettierrc
6 | jest.config.ts
7 | src
8 | tests
9 | tsconfig.json
--------------------------------------------------------------------------------
/packages/email-builder/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Waypoint (Metaccountant, Inc.)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/email-builder/README.md:
--------------------------------------------------------------------------------
1 | # usewaypoint/email-builder
2 |
--------------------------------------------------------------------------------
/packages/email-builder/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@usewaypoint/email-builder",
3 | "version": "0.0.8",
4 | "description": "React component to render email messages",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.mjs",
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": {
10 | "require": "./dist/index.js",
11 | "import": "./dist/index.mjs",
12 | "types": "./dist/index.d.ts"
13 | }
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "scripts": {
19 | "build": "npx tsc"
20 | },
21 | "author": "carlos@usewaypoint.com",
22 | "license": "MIT",
23 | "peerDependencies": {
24 | "react": "^16 || ^17 || ^18",
25 | "react-dom": "^16 || ^17 || ^18",
26 | "zod": "^1 || ^2 || ^3"
27 | },
28 | "dependencies": {
29 | "@usewaypoint/block-avatar": "^0.0.3",
30 | "@usewaypoint/block-button": "^0.0.3",
31 | "@usewaypoint/block-columns-container": "^0.0.3",
32 | "@usewaypoint/block-container": "^0.0.2",
33 | "@usewaypoint/block-divider": "^0.0.4",
34 | "@usewaypoint/block-heading": "^0.0.3",
35 | "@usewaypoint/block-html": "^0.0.3",
36 | "@usewaypoint/block-image": "^0.0.5",
37 | "@usewaypoint/block-spacer": "^0.0.3",
38 | "@usewaypoint/block-text": "^0.0.6",
39 | "@usewaypoint/document-core": "^0.0.6"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerPropsSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ColumnsContainerPropsSchema as BaseColumnsContainerPropsSchema } from '@usewaypoint/block-columns-container';
4 |
5 | const BasePropsShape = BaseColumnsContainerPropsSchema.shape.props.unwrap().unwrap().shape;
6 |
7 | const ColumnsContainerPropsSchema = z.object({
8 | style: BaseColumnsContainerPropsSchema.shape.style,
9 | props: z
10 | .object({
11 | ...BasePropsShape,
12 | columns: z.tuple([
13 | z.object({ childrenIds: z.array(z.string()) }),
14 | z.object({ childrenIds: z.array(z.string()) }),
15 | z.object({ childrenIds: z.array(z.string()) }),
16 | ]),
17 | })
18 | .optional()
19 | .nullable(),
20 | });
21 |
22 | export default ColumnsContainerPropsSchema;
23 | export type ColumnsContainerProps = z.infer<typeof ColumnsContainerPropsSchema>;
24 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/ColumnsContainer/ColumnsContainerReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ColumnsContainer as BaseColumnsContainer } from '@usewaypoint/block-columns-container';
4 |
5 | import { ReaderBlock } from '../../Reader/core';
6 |
7 | import { ColumnsContainerProps } from './ColumnsContainerPropsSchema';
8 |
9 | export default function ColumnsContainerReader({ style, props }: ColumnsContainerProps) {
10 | const { columns, ...restProps } = props ?? {};
11 | let cols = undefined;
12 | if (columns) {
13 | cols = columns.map((col) => col.childrenIds.map((childId) => <ReaderBlock key={childId} id={childId} />));
14 | }
15 |
16 | return <BaseColumnsContainer props={restProps} columns={cols} style={style} />;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/Container/ContainerPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { ContainerPropsSchema as BaseContainerPropsSchema } from '@usewaypoint/block-container';
4 |
5 | export const ContainerPropsSchema = z.object({
6 | style: BaseContainerPropsSchema.shape.style,
7 | props: z
8 | .object({
9 | childrenIds: z.array(z.string()).optional().nullable(),
10 | })
11 | .optional()
12 | .nullable(),
13 | });
14 |
15 | export type ContainerProps = z.infer<typeof ContainerPropsSchema>;
16 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/Container/ContainerReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container as BaseContainer } from '@usewaypoint/block-container';
4 |
5 | import { ReaderBlock } from '../../Reader/core';
6 |
7 | import { ContainerProps } from './ContainerPropsSchema';
8 |
9 | export default function ContainerReader({ style, props }: ContainerProps) {
10 | const childrenIds = props?.childrenIds ?? [];
11 | return (
12 | <BaseContainer style={style}>
13 | {childrenIds.map((childId) => (
14 | <ReaderBlock key={childId} id={childId} />
15 | ))}
16 | </BaseContainer>
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/EmailLayout/EmailLayoutPropsSchema.tsx:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const COLOR_SCHEMA = z
4 | .string()
5 | .regex(/^#[0-9a-fA-F]{6}$/)
6 | .nullable()
7 | .optional();
8 |
9 | const FONT_FAMILY_SCHEMA = z
10 | .enum([
11 | 'MODERN_SANS',
12 | 'BOOK_SANS',
13 | 'ORGANIC_SANS',
14 | 'GEOMETRIC_SANS',
15 | 'HEAVY_SANS',
16 | 'ROUNDED_SANS',
17 | 'MODERN_SERIF',
18 | 'BOOK_SERIF',
19 | 'MONOSPACE',
20 | ])
21 | .nullable()
22 | .optional();
23 |
24 | export const EmailLayoutPropsSchema = z.object({
25 | backdropColor: COLOR_SCHEMA,
26 | borderColor: COLOR_SCHEMA,
27 | borderRadius: z.number().optional().nullable(),
28 | canvasColor: COLOR_SCHEMA,
29 | textColor: COLOR_SCHEMA,
30 | fontFamily: FONT_FAMILY_SCHEMA,
31 | childrenIds: z.array(z.string()).optional().nullable(),
32 | });
33 |
34 | export type EmailLayoutProps = z.infer<typeof EmailLayoutPropsSchema>;
35 |
--------------------------------------------------------------------------------
/packages/email-builder/src/blocks/EmailLayout/EmailLayoutReader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ReaderBlock } from '../../Reader/core';
4 |
5 | import { EmailLayoutProps } from './EmailLayoutPropsSchema';
6 |
7 | function getFontFamily(fontFamily: EmailLayoutProps['fontFamily']) {
8 | const f = fontFamily ?? 'MODERN_SANS';
9 | switch (f) {
10 | case 'MODERN_SANS':
11 | return '"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif';
12 | case 'BOOK_SANS':
13 | return 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif';
14 | case 'ORGANIC_SANS':
15 | return 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, sans-serif';
16 | case 'GEOMETRIC_SANS':
17 | return 'Avenir, "Avenir Next LT Pro", Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif';
18 | case 'HEAVY_SANS':
19 | return 'Bahnschrift, "DIN Alternate", "Franklin Gothic Medium", "Nimbus Sans Narrow", sans-serif-condensed, sans-serif';
20 | case 'ROUNDED_SANS':
21 | return 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif';
22 | case 'MODERN_SERIF':
23 | return 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif';
24 | case 'BOOK_SERIF':
25 | return '"Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif';
26 | case 'MONOSPACE':
27 | return '"Nimbus Mono PS", "Courier New", "Cutive Mono", monospace';
28 | }
29 | }
30 |
31 | function getBorder({ borderColor }: EmailLayoutProps) {
32 | if (!borderColor) {
33 | return undefined;
34 | }
35 | return `1px solid ${borderColor}`;
36 | }
37 |
38 | export default function EmailLayoutReader(props: EmailLayoutProps) {
39 | const childrenIds = props.childrenIds ?? [];
40 | return (
41 | <div
42 | style={{
43 | backgroundColor: props.backdropColor ?? '#F5F5F5',
44 | color: props.textColor ?? '#262626',
45 | fontFamily: getFontFamily(props.fontFamily),
46 | fontSize: '16px',
47 | fontWeight: '400',
48 | letterSpacing: '0.15008px',
49 | lineHeight: '1.5',
50 | margin: '0',
51 | padding: '32px 0',
52 | minHeight: '100%',
53 | width: '100%',
54 | }}
55 | >
56 | <table
57 | align="center"
58 | width="100%"
59 | style={{
60 | margin: '0 auto',
61 | maxWidth: '600px',
62 | backgroundColor: props.canvasColor ?? '#FFFFFF',
63 | borderRadius: props.borderRadius ?? undefined,
64 | border: getBorder(props),
65 | }}
66 | role="presentation"
67 | cellSpacing="0"
68 | cellPadding="0"
69 | border={0}
70 | >
71 | <tbody>
72 | <tr style={{ width: '100%' }}>
73 | <td>
74 | {childrenIds.map((childId) => (
75 | <ReaderBlock key={childId} id={childId} />
76 | ))}
77 | </td>
78 | </tr>
79 | </tbody>
80 | </table>
81 | </div>
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/packages/email-builder/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as renderToStaticMarkup } from './renderers/renderToStaticMarkup';
2 |
3 | export {
4 | ReaderBlockSchema,
5 | TReaderBlock,
6 | //
7 | ReaderDocumentSchema,
8 | TReaderDocument,
9 | //
10 | ReaderBlock,
11 | TReaderBlockProps,
12 | //
13 | TReaderProps,
14 | default as Reader,
15 | } from './Reader/core';
16 |
--------------------------------------------------------------------------------
/packages/email-builder/src/renderers/renderToStaticMarkup.spec.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | import renderToStaticMarkup from './renderToStaticMarkup';
6 |
7 | describe('renderToStaticMarkup', () => {
8 | it('renders into a string', () => {
9 | const result = renderToStaticMarkup(
10 | {
11 | root: {
12 | type: 'Container',
13 | data: {
14 | props: {
15 | childrenIds: [],
16 | },
17 | },
18 | },
19 | },
20 | { rootBlockId: 'root' }
21 | );
22 | expect(result).toEqual('<!DOCTYPE html><html><body><div></div></body></html>');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/email-builder/src/renderers/renderToStaticMarkup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToStaticMarkup as baseRenderToStaticMarkup } from 'react-dom/server';
3 |
4 | import Reader, { TReaderDocument } from '../Reader/core';
5 |
6 | type TOptions = {
7 | rootBlockId: string;
8 | };
9 | export default function renderToStaticMarkup(document: TReaderDocument, { rootBlockId }: TOptions) {
10 | return (
11 | '<!DOCTYPE html>' +
12 | baseRenderToStaticMarkup(
13 | <html>
14 | <body>
15 | <Reader document={document} rootBlockId={rootBlockId} />
16 | </body>
17 | </html>
18 | )
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/email-builder/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["tests/**/*.spec.ts", "tests/**/*.spec.tsx", "jest.config.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/email-builder/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2015",
5 | "module": "esnext",
6 | "outDir": "dist"
7 | },
8 | "exclude": ["dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "esnext",
5 | "lib": [],
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "strict": true,
9 | "sourceMap": true,
10 | "allowJs": true,
11 | "esModuleInterop": true,
12 |
13 | "skipLibCheck": true,
14 | "declarationMap": true,
15 | "declaration": true,
16 | "noUnusedLocals": true,
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | "allowSyntheticDefaultImports": true
20 | },
21 | "exclude": ["dist"]
22 | }
23 |
--------------------------------------------------------------------------------