The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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]&gt;&lt;i style="letter-spacing: 20px;mso-font-width:-100%;mso-text-raise:30" hidden&gt;&nbsp;&lt;/i&gt;&lt;![endif]-->
13 |       </span>
14 |       <span />
15 |       <span>
16 |         <!--[if mso]&gt;&lt;i style="letter-spacing: 20px;mso-font-width:-100%" hidden&gt;&nbsp;&lt;/i&gt;&lt;![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 &lt;span&gt;markdown&lt;/span&gt;
 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 | ![Uh oh...]("onerror="alert('XSS'))
33 | ![Uh oh...](https://www.example.com/image.png"onload="alert('XSS'))
34 | ![Escape SRC - onload](https://www.example.com/image.png"onload="alert('ImageOnLoad'))
35 | ![Escape SRC - onerror]("onerror="alert('ImageOnError'))
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 &mdash; 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 &apos;pro&apos; 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 | 


--------------------------------------------------------------------------------