From 2ba98b74ecc834e6a23ffb9e90858657e573c1fa Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 10:26:05 -0500 Subject: [PATCH 01/38] Initial React Email Exporter Project Structure --- packages/xl-react-email-exporter/.gitignore | 24 + packages/xl-react-email-exporter/package.json | 86 ++++ packages/xl-react-email-exporter/src/index.ts | 0 .../xl-react-email-exporter/src/vite-env.d.ts | 11 + .../xl-react-email-exporter/tsconfig.json | 45 ++ .../xl-react-email-exporter/vite.config.ts | 65 +++ packages/xl-react-email-exporter/viteSetup.ts | 10 + pnpm-lock.yaml | 431 +++++++++++++++++- 8 files changed, 667 insertions(+), 5 deletions(-) create mode 100644 packages/xl-react-email-exporter/.gitignore create mode 100644 packages/xl-react-email-exporter/package.json create mode 100644 packages/xl-react-email-exporter/src/index.ts create mode 100644 packages/xl-react-email-exporter/src/vite-env.d.ts create mode 100644 packages/xl-react-email-exporter/tsconfig.json create mode 100644 packages/xl-react-email-exporter/vite.config.ts create mode 100644 packages/xl-react-email-exporter/viteSetup.ts diff --git a/packages/xl-react-email-exporter/.gitignore b/packages/xl-react-email-exporter/.gitignore new file mode 100644 index 0000000000..54f07af58b --- /dev/null +++ b/packages/xl-react-email-exporter/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/packages/xl-react-email-exporter/package.json b/packages/xl-react-email-exporter/package.json new file mode 100644 index 0000000000..0e387e5871 --- /dev/null +++ b/packages/xl-react-email-exporter/package.json @@ -0,0 +1,86 @@ +{ + "name": "@blocknote/xl-react-email-exporter", + "homepage": "https://github.com/TypeCellOS/BlockNote", + "private": true, + "license": "AGPL-3.0", + "version": "0.18.1", + "files": [ + "dist", + "types", + "src" + ], + "keywords": [ + "react", + "javascript", + "editor", + "typescript", + "prosemirror", + "wysiwyg", + "rich-text-editor", + "notion", + "yjs", + "block-based", + "tiptap" + ], + "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-xl-react-email-exporter.umd.cjs", + "module": "./dist/blocknote-xl-react-email-exporter.js", + "exports": { + ".": { + "types": "./types/src/index.d.ts", + "import": "./dist/blocknote-xl-react-email-exporter.js", + "require": "./dist/blocknote-xl-react-email-exporter.umd.cjs" + }, + "./style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --max-warnings 0", + "test": "vitest --run", + "test-watch": "vitest watch", + "email": "email dev" + }, + "dependencies": { + "@blocknote/core": "0.31.3", + "@blocknote/react": "0.31.3", + "buffer": "^6.0.3", + "react": "^18", + "react-dom": "^18", + "react-email": "^4.0.16", + "@react-email/components": "^0.1.0", + "@react-email/render": "^1.1.2" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "eslint": "^8.10.0", + "prettier": "^2.7.1", + "rollup-plugin-webpack-stats": "^0.2.2", + "typescript": "^5.0.4", + "vite": "^5.3.4", + "vite-plugin-eslint": "^1.8.1", + "vitest": "^2.0.3" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.js" + ] + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" +} \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/index.ts b/packages/xl-react-email-exporter/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/xl-react-email-exporter/src/vite-env.d.ts b/packages/xl-react-email-exporter/src/vite-env.d.ts new file mode 100644 index 0000000000..b12ff18be4 --- /dev/null +++ b/packages/xl-react-email-exporter/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ImportMetaEnv { + // readonly VITE_APP_TITLE: string; + // more env variables... +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/xl-react-email-exporter/tsconfig.json b/packages/xl-react-email-exporter/tsconfig.json new file mode 100644 index 0000000000..d8f00bf6a3 --- /dev/null +++ b/packages/xl-react-email-exporter/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ESNext", + "DOM" + ], + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true, + "paths": { + "@shared/*": [ + "../../shared/*" + ] + } + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core" + }, + { + "path": "../react" + }, + { + "path": "../../shared" + } + ] +} \ No newline at end of file diff --git a/packages/xl-react-email-exporter/vite.config.ts b/packages/xl-react-email-exporter/vite.config.ts new file mode 100644 index 0000000000..9d5ea9f7d8 --- /dev/null +++ b/packages/xl-react-email-exporter/vite.config.ts @@ -0,0 +1,65 @@ +import * as path from "path"; +import { webpackStats } from "rollup-plugin-webpack-stats"; +import { defineConfig } from "vite"; +import pkg from "./package.json"; +// import eslintPlugin from "vite-plugin-eslint"; + +const deps = Object.keys(pkg.dependencies); + +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + test: { + setupFiles: ["./vitestSetup.ts"], + // assetsInclude: [ + // "**/*.woff", + // "**/*.woff2", + // "**/*.ttf", + // "**/*.otf", + // ], // Add other font extensions if needed + }, + plugins: [webpackStats() as any], + // used so that vitest resolves the core package from the sources instead of the built version + resolve: { + alias: + conf.command === "build" + ? ({ + "@shared": path.resolve(__dirname, "../../shared/"), + } as Record) + : ({ + "@shared": path.resolve(__dirname, "../../shared/"), + // load live from sources with live reload working + "@blocknote/core": path.resolve(__dirname, "../core/src/"), + "@blocknote/react": path.resolve(__dirname, "../react/src/"), + } as Record), + }, + server: { + fs: { + allow: ["../../shared"], // Allows access to `shared/assets` + }, + }, + build: { + // assetsInclude: ["**/*.woff", "**/*.woff2", "**/*.ttf", "**/*.otf"], // Add other font extensions if needed + sourcemap: true, + lib: { + entry: path.resolve(__dirname, "src/index.ts"), + name: "blocknote-xl-react-email-exporter", + fileName: "blocknote-xl-react-email-exporter", + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: (source: string) => { + if (deps.includes(source)) { + return true; + } + return source.startsWith("prosemirror-"); + }, + output: { + // Provide global variables to use in the UMD build + // for externalized deps + globals: {}, + interop: "compat", // https://rollupjs.org/migration/#changed-defaults + }, + }, + }, +})); diff --git a/packages/xl-react-email-exporter/viteSetup.ts b/packages/xl-react-email-exporter/viteSetup.ts new file mode 100644 index 0000000000..a946b5fc3a --- /dev/null +++ b/packages/xl-react-email-exporter/viteSetup.ts @@ -0,0 +1,10 @@ +import { afterEach, beforeEach } from "vitest"; + +beforeEach(() => { + globalThis.window = globalThis.window || ({} as any); + (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {}; +}); + +afterEach(() => { + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d15352845..6531e52b5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4139,6 +4139,64 @@ importers: specifier: ^2.0.3 version: 2.1.9(@types/node@22.14.1)(@vitest/ui@2.1.9)(jsdom@25.0.1(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(msw@2.7.3(@types/node@22.14.1)(typescript@5.8.2))(terser@5.39.2) + packages/xl-react-email-exporter: + dependencies: + '@blocknote/core': + specifier: 0.31.3 + version: link:../core + '@blocknote/react': + specifier: 0.31.3 + version: link:../react + '@react-email/components': + specifier: ^0.1.0 + version: 0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/render': + specifier: ^1.1.2 + version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + react-email: + specifier: ^4.0.16 + version: 4.0.16(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 + '@types/react': + specifier: ^18.0.25 + version: 18.3.20 + '@types/react-dom': + specifier: ^18.0.9 + version: 18.3.5(@types/react@18.3.20) + eslint: + specifier: ^8.10.0 + version: 8.57.1 + prettier: + specifier: ^2.7.1 + version: 2.8.8 + rollup-plugin-webpack-stats: + specifier: ^0.2.2 + version: 0.2.6(rollup@4.37.0) + typescript: + specifier: ^5.0.4 + version: 5.8.2 + vite: + specifier: ^5.3.4 + version: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) + vite-plugin-eslint: + specifier: ^1.8.1 + version: 1.8.1(eslint@8.57.1)(vite@5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2)) + vitest: + specifier: ^2.0.3 + version: 2.1.9(@types/node@22.14.1)(@vitest/ui@2.1.9)(jsdom@25.0.1(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(msw@2.7.3(@types/node@22.14.1)(typescript@5.8.2))(terser@5.39.2) + playground: dependencies: '@ai-sdk/anthropic': @@ -4364,7 +4422,7 @@ importers: version: link:../packages/shadcn '@playwright/experimental-ct-react': specifier: 1.51.1 - version: 1.51.1(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2)(vite@5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2)) + version: 1.51.1(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(vite@5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2))(yaml@2.7.0) '@playwright/test': specifier: 1.51.1 version: 1.51.1 @@ -6424,6 +6482,14 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -7612,12 +7678,24 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/button@0.1.0': + resolution: {integrity: sha512-fg4LtgTu5zXxaRSly9cuv6sHVF/hi1lElbRaIA8EPx5coWOBhCto6rCPfawcXpaN2oER7rNHUrcNBkI+lz5F9A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-block@0.0.12': resolution: {integrity: sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-block@0.1.0': + resolution: {integrity: sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-inline@0.0.5': resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} engines: {node: '>=18.0.0'} @@ -7636,6 +7714,12 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/components@0.1.0': + resolution: {integrity: sha512-Rx0eZk0XuzLKXC5NoMm8xuH72ALVsPYNb/BvcdCJx4EZAoVpQISb4sCqpo9blVYVIazNr4MqWroqFb3ZNrCLMQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/container@0.0.15': resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} engines: {node: '>=18.0.0'} @@ -7689,12 +7773,24 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/markdown@0.0.15': + resolution: {integrity: sha512-UQA9pVm5sbflgtg3EX3FquUP4aMBzmLReLbGJ6DZQZnAskBF36aI56cRykDq1o+1jT+CKIK1CducPYziaXliag==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/preview@0.0.12': resolution: {integrity: sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/preview@0.0.13': + resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/render@1.0.6': resolution: {integrity: sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==} engines: {node: '>=18.0.0'} @@ -7702,6 +7798,13 @@ packages: react: ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/render@1.1.2': + resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/row@0.0.12': resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} engines: {node: '>=18.0.0'} @@ -7720,12 +7823,24 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/tailwind@1.0.5': + resolution: {integrity: sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/text@0.1.1': resolution: {integrity: sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/text@0.1.5': + resolution: {integrity: sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-pdf/fns@3.1.2': resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==} @@ -9617,6 +9732,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} @@ -9705,6 +9824,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -10806,6 +10929,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -10887,6 +11014,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -11286,6 +11418,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -11387,6 +11523,14 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} @@ -11437,6 +11581,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + jake@10.9.2: resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} engines: {node: '>=10'} @@ -11746,6 +11894,14 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -11759,6 +11915,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -12126,6 +12286,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-match@1.0.2: resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==} @@ -12137,6 +12301,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -12145,6 +12313,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@2.1.0: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} engines: {node: '>=8'} @@ -12161,6 +12333,10 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -12515,6 +12691,10 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + oniguruma-parser@0.5.4: resolution: {integrity: sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA==} @@ -12545,6 +12725,10 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} @@ -12654,6 +12838,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} @@ -12930,6 +13118,11 @@ packages: prettier-plugin-svelte: optional: true + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -13130,6 +13323,11 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-email@4.0.16: + resolution: {integrity: sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg==} + engines: {node: '>=18.0.0'} + hasBin: true + react-email@4.0.7: resolution: {integrity: sha512-XCXlfZLKv9gHd/ZwUEhCpRGc/FJLZGYczeuG1kVR/be2PlkwEB4gjX9ARBbRFv86ncbtpOu/wI6jD6kadRyAKw==} engines: {node: '>=18.0.0'} @@ -13420,6 +13618,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + restructure@3.0.2: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} @@ -13782,6 +13984,10 @@ packages: std-env@3.8.1: resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -13804,6 +14010,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} @@ -14841,6 +15051,10 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} @@ -17098,6 +17312,12 @@ snapshots: optionalDependencies: '@types/node': 22.14.1 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -17964,13 +18184,14 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/experimental-ct-core@1.51.1(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2)': + '@playwright/experimental-ct-core@1.51.1(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0)': dependencies: playwright: 1.51.1 playwright-core: 1.51.1 - vite: 5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2) + vite: 6.3.5(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' + - jiti - less - lightningcss - sass @@ -17978,13 +18199,16 @@ snapshots: - stylus - sugarss - terser + - tsx + - yaml - '@playwright/experimental-ct-react@1.51.1(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2)(vite@5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2))': + '@playwright/experimental-ct-react@1.51.1(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(vite@5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2))(yaml@2.7.0)': dependencies: - '@playwright/experimental-ct-core': 1.51.1(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2) + '@playwright/experimental-ct-core': 1.51.1(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0) '@vitejs/plugin-react': 4.4.1(vite@5.4.15(@types/node@20.17.28)(lightningcss@1.30.1)(terser@5.39.2)) transitivePeerDependencies: - '@types/node' + - jiti - less - lightningcss - sass @@ -17993,7 +18217,9 @@ snapshots: - sugarss - supports-color - terser + - tsx - vite + - yaml '@playwright/test@1.51.1': dependencies: @@ -18438,11 +18664,20 @@ snapshots: dependencies: react: 18.3.1 + '@react-email/button@0.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@react-email/code-block@0.0.12(react@18.3.1)': dependencies: prismjs: 1.30.0 react: 18.3.1 + '@react-email/code-block@0.1.0(react@18.3.1)': + dependencies: + prismjs: 1.30.0 + react: 18.3.1 + '@react-email/code-inline@0.0.5(react@18.3.1)': dependencies: react: 18.3.1 @@ -18477,6 +18712,32 @@ snapshots: transitivePeerDependencies: - react-dom + '@react-email/components@0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-email/body': 0.0.11(react@18.3.1) + '@react-email/button': 0.1.0(react@18.3.1) + '@react-email/code-block': 0.1.0(react@18.3.1) + '@react-email/code-inline': 0.0.5(react@18.3.1) + '@react-email/column': 0.0.13(react@18.3.1) + '@react-email/container': 0.0.15(react@18.3.1) + '@react-email/font': 0.0.9(react@18.3.1) + '@react-email/head': 0.0.12(react@18.3.1) + '@react-email/heading': 0.0.15(react@18.3.1) + '@react-email/hr': 0.0.11(react@18.3.1) + '@react-email/html': 0.0.11(react@18.3.1) + '@react-email/img': 0.0.11(react@18.3.1) + '@react-email/link': 0.0.12(react@18.3.1) + '@react-email/markdown': 0.0.15(react@18.3.1) + '@react-email/preview': 0.0.13(react@18.3.1) + '@react-email/render': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/row': 0.0.12(react@18.3.1) + '@react-email/section': 0.0.16(react@18.3.1) + '@react-email/tailwind': 1.0.5(react@18.3.1) + '@react-email/text': 0.1.5(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - react-dom + '@react-email/container@0.0.15(react@18.3.1)': dependencies: react: 18.3.1 @@ -18514,10 +18775,19 @@ snapshots: md-to-react-email: 5.0.5(react@18.3.1) react: 18.3.1 + '@react-email/markdown@0.0.15(react@18.3.1)': + dependencies: + md-to-react-email: 5.0.5(react@18.3.1) + react: 18.3.1 + '@react-email/preview@0.0.12(react@18.3.1)': dependencies: react: 18.3.1 + '@react-email/preview@0.0.13(react@18.3.1)': + dependencies: + react: 18.3.1 + '@react-email/render@1.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: html-to-text: 9.0.5 @@ -18526,6 +18796,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-promise-suspense: 0.3.4 + '@react-email/render@1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.5.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-promise-suspense: 0.3.4 + '@react-email/row@0.0.12(react@18.3.1)': dependencies: react: 18.3.1 @@ -18538,10 +18816,18 @@ snapshots: dependencies: react: 18.3.1 + '@react-email/tailwind@1.0.5(react@18.3.1)': + dependencies: + react: 18.3.1 + '@react-email/text@0.1.1(react@18.3.1)': dependencies: react: 18.3.1 + '@react-email/text@0.1.5(react@18.3.1)': + dependencies: + react: 18.3.1 + '@react-pdf/fns@3.1.2': {} '@react-pdf/font@4.0.2': @@ -20862,6 +21148,10 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.6.1: {} cli-spinners@2.9.2: {} @@ -20950,6 +21240,8 @@ snapshots: commander@11.1.0: {} + commander@13.1.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -22296,6 +22588,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.3.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -22391,6 +22685,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -22901,6 +23204,8 @@ snapshots: is-interactive@1.0.0: {} + is-interactive@2.0.0: {} + is-map@2.0.3: {} is-mobile@3.1.1: {} @@ -22980,6 +23285,10 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} is-weakmap@2.0.2: {} @@ -23034,6 +23343,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + jake@10.9.2: dependencies: async: 3.2.6 @@ -23369,6 +23682,16 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-symbols@6.0.0: + dependencies: + chalk: 5.4.1 + is-unicode-supported: 1.3.0 + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.1 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -23379,6 +23702,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.1.0: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -24215,6 +24540,8 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-match@1.0.2: dependencies: wildcard: 1.1.2 @@ -24227,10 +24554,16 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} + mimic-response@2.1.0: optional: true @@ -24257,6 +24590,10 @@ snapshots: minimalistic-assert@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -24707,6 +25044,10 @@ snapshots: dependencies: mimic-fn: 4.0.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + oniguruma-parser@0.5.4: {} oniguruma-to-es@4.1.0: @@ -24763,6 +25104,18 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + ora@8.2.0: + dependencies: + chalk: 5.4.1 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + orderedmap@2.1.1: {} outvariant@1.4.3: {} @@ -24882,6 +25235,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.2 + path-to-regexp@3.3.0: {} path-to-regexp@6.3.0: {} @@ -25086,6 +25444,8 @@ snapshots: dependencies: prettier: 3.5.3 + prettier@2.8.8: {} + prettier@3.5.3: {} pretty-format@27.5.1: @@ -25298,6 +25658,35 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 + react-email@4.0.16(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/parser': 7.27.0 + '@babel/traverse': 7.27.0 + chalk: 5.4.1 + chokidar: 4.0.3 + commander: 13.1.0 + debounce: 2.0.0 + esbuild: 0.25.1 + glob: 11.0.3 + log-symbols: 7.0.1 + mime-types: 3.0.1 + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + normalize-path: 3.0.0 + ora: 8.2.0 + socket.io: 4.8.1 + transitivePeerDependencies: + - '@babel/core' + - '@opentelemetry/api' + - '@playwright/test' + - babel-plugin-macros + - babel-plugin-react-compiler + - bufferutil + - react + - react-dom + - sass + - supports-color + - utf-8-validate + react-email@4.0.7(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/parser': 7.24.5 @@ -25709,6 +26098,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + restructure@3.0.2: {} retry@0.13.1: {} @@ -26195,6 +26589,8 @@ snapshots: std-env@3.8.1: {} + stdin-discarder@0.2.2: {} + stoppable@1.1.0: {} streamsearch@1.1.0: {} @@ -26215,6 +26611,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + string.prototype.codepointat@0.2.1: {} string.prototype.includes@2.0.1: @@ -27031,6 +27433,23 @@ snapshots: lightningcss: 1.30.1 terser: 5.39.2 + vite@6.3.5(@types/node@20.17.28)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0): + dependencies: + esbuild: 0.25.1 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.37.0 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 20.17.28 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.39.2 + tsx: 4.19.3 + yaml: 2.7.0 + vite@6.3.5(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0): dependencies: esbuild: 0.25.1 @@ -27470,6 +27889,8 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yoctocolors@2.1.1: {} + yoga-layout@3.2.1: {} yoga-wasm-web@0.3.3: {} From 2f34c7044279bf48aa8fe37cfa8556d2ff01facf Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 11:16:30 -0500 Subject: [PATCH 02/38] Pulling in Structure for Email Schema and Example Structure --- .../.bnexample.json | 12 + .../App.tsx | 358 ++++++++++++++++++ .../README.md | 5 + .../index.html | 17 + .../main.tsx | 11 + .../package.json | 28 ++ .../styles.css | 25 ++ .../tsconfig.json | 36 ++ .../vite.config.ts | 32 ++ packages/xl-react-email-exporter/src/index.ts | 1 + .../src/react-email/defaultSchema/blocks.tsx | 83 ++++ .../src/react-email/defaultSchema/index.ts | 9 + .../defaultSchema/inlinecontent.tsx | 26 ++ .../src/react-email/defaultSchema/styles.tsx | 63 +++ .../src/react-email/index.ts | 2 + .../react-email/reactEmailExporter.test.tsx | 130 +++++++ .../src/react-email/reactEmailExporter.tsx | 136 +++++++ playground/package.json | 3 +- pnpm-lock.yaml | 54 +++ 19 files changed, 1030 insertions(+), 1 deletion(-) create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/README.md create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/index.html create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/main.tsx create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/package.json create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/styles.css create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/tsconfig.json create mode 100644 examples/05-interoperability/08-converting-blocks-to-react-email/vite.config.ts create mode 100644 packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx create mode 100644 packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts create mode 100644 packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx create mode 100644 packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx create mode 100644 packages/xl-react-email-exporter/src/react-email/index.ts create mode 100644 packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx create mode 100644 packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json new file mode 100644 index 0000000000..02fc1193cc --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json @@ -0,0 +1,12 @@ +{ + "playground": true, + "docs": true, + "author": "jmarbutt", + "tags": [ + "" + ], + "dependencies": { + "@blocknote/xl-react-email-exporter": "latest" + }, + "pro": true +} \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx new file mode 100644 index 0000000000..a779b62001 --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx @@ -0,0 +1,358 @@ +import { + BlockNoteSchema, + combineByGroup, + filterSuggestionItems, + withPageBreak, +} from "@blocknote/core"; +import "@blocknote/core/fonts/inter.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { + SuggestionMenuController, + getDefaultReactSlashMenuItems, + getPageBreakReactSlashMenuItems, + useCreateBlockNote, +} from "@blocknote/react"; +import { + ReactEmailExporter, + reactEmailDefaultSchemaMappings, +} from "@blocknote/xl-react-email-exporter"; +import { useEffect, useMemo, useState } from "react"; + +import "./styles.css"; + +export default function App() { + // Stores the editor's contents as HTML. + const [emailDocument, setEmailDocument] = useState(); + + // Creates a new editor instance with some initial content. + const editor = useCreateBlockNote({ + schema: withPageBreak(BlockNoteSchema.create()), + tables: { + splitCells: true, + cellBackgroundColor: true, + cellTextColor: true, + headers: true, + }, + initialContent: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Welcome to this ", + styles: { + italic: true, + }, + }, + { + type: "text", + text: "demo!", + styles: { + italic: true, + bold: true, + }, + }, + ], + children: [ + { + type: "paragraph", + content: "Hello World nested", + children: [ + { + type: "paragraph", + content: "Hello World double nested", + }, + ], + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "This paragraph has a background color", + styles: { bold: true }, + }, + ], + props: { + backgroundColor: "red", + }, + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "This one too, but it's blue", + styles: { italic: true }, + }, + ], + props: { + backgroundColor: "blue", + }, + }, + { + type: "paragraph", + content: "Paragraph", + }, + { + type: "heading", + content: "Heading", + }, + { + type: "heading", + content: "Heading right", + props: { + textAlignment: "right", + }, + }, + { + type: "paragraph", + content: + "justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + + props: { + textAlignment: "justify", + }, + }, + { + type: "bulletListItem", + content: + "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + children: [ + { + type: "bulletListItem", + content: + "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + }, + { + type: "bulletListItem", + content: + "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + props: { + textAlignment: "right", + }, + }, + { + type: "numberedListItem", + content: "Numbered List Item 1", + }, + { + type: "numberedListItem", + content: "Numbered List Item 2", + children: [ + { + type: "numberedListItem", + content: "Numbered List Item Nested 1", + }, + { + type: "numberedListItem", + content: "Numbered List Item Nested 2", + }, + { + type: "numberedListItem", + content: "Numbered List Item Nested funky right", + props: { + textAlignment: "right", + backgroundColor: "red", + textColor: "blue", + }, + }, + { + type: "numberedListItem", + content: "Numbered List Item Nested funky center", + props: { + textAlignment: "center", + backgroundColor: "red", + textColor: "blue", + }, + }, + ], + }, + ], + }, + { + type: "numberedListItem", + content: "Numbered List Item", + }, + { + type: "checkListItem", + content: "Check List Item", + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + { + cells: ["Table Cell", "Table Cell", "Table Cell"], + }, + ], + }, + }, + { + type: "pageBreak", + }, + { + type: "file", + }, + { + type: "image", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", + }, + }, + { + type: "image", + props: { + previewWidth: 200, + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg", + textAlignment: "right", + }, + }, + { + type: "video", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm", + }, + }, + { + type: "audio", + props: { + url: "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + caption: + "From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3", + }, + }, + { + type: "paragraph", + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Inline Content:", + styles: { bold: true }, + }, + ], + }, + { + type: "paragraph", + content: [ + { + type: "text", + text: "Styled Text", + styles: { + bold: true, + italic: true, + textColor: "red", + backgroundColor: "blue", + }, + }, + { + type: "text", + text: " ", + styles: {}, + }, + { + type: "link", + content: "Link", + href: "https://www.blocknotejs.org", + }, + ], + }, + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: ["Table Cell 1", "Table Cell 2", "Table Cell 3"], + }, + { + cells: [ + "Table Cell 4", + [ + { + type: "text", + text: "Table Cell Bold 5", + styles: { + bold: true, + }, + }, + ], + "Table Cell 6", + ], + }, + { + cells: ["Table Cell 7", "Table Cell 8", "Table Cell 9"], + }, + ], + }, + }, + { + type: "codeBlock", + props: { + language: "javascript", + }, + content: `const helloWorld = (message) => { + console.log("Hello World", message); +};`, + }, + ], + }); + + const onChange = async () => { + const exporter = new ReactEmailExporter( + editor.schema, + reactEmailDefaultSchemaMappings, + ); + // Converts the editor's contents from Block objects to HTML and store to state. + const emailDocument = await exporter.toReactEmailDocument(editor.document); + setEmailDocument(emailDocument); + + // const blob = await ReactPDF.pdf(pdfDocument).toBlob(); + }; + + useEffect(() => { + onChange(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const slashMenuItems = useMemo(() => { + return combineByGroup( + getDefaultReactSlashMenuItems(editor), + getPageBreakReactSlashMenuItems(editor), + ); + }, [editor]); + + // Renders the editor instance, and its contents as HTML below. + return ( +
+
+ + + filterSuggestionItems(slashMenuItems, query) + } + /> + +
+
Email Preview Here
+
+ ); +} diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/README.md b/examples/05-interoperability/08-converting-blocks-to-react-email/README.md new file mode 100644 index 0000000000..39cad64a26 --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/README.md @@ -0,0 +1,5 @@ +# Exporting documents to React Email + +This example exports the current document (all blocks) as a React Email document. + +**Try it out:** Edit the document and the preview will update. diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/index.html b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html new file mode 100644 index 0000000000..86659f0b5c --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html @@ -0,0 +1,17 @@ + + + + + + + Exporting documents to React Email + + + +
+ + + + \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/main.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/main.tsx new file mode 100644 index 0000000000..6284417d60 --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json new file mode 100644 index 0000000000..4d939c63a1 --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -0,0 +1,28 @@ +{ + "name": "@blocknote/example-interoperability-converting-react-email", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/core": "latest", + "@blocknote/react": "latest", + "@blocknote/ariakit": "latest", + "@blocknote/mantine": "latest", + "@blocknote/shadcn": "latest", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@blocknote/xl-react-email-exporter": "latest" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.4" + } +} \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css b/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css new file mode 100644 index 0000000000..6d5eeba7fe --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css @@ -0,0 +1,25 @@ +.wrapper { + display: flex; + flex-direction: column; + height: 100%; +} + +.item { + border-radius: 0.5rem; + flex: 1; + overflow: hidden; +} + +.item.bordered { + border: 1px solid gray; +} + +.item pre { + border-radius: 0.5rem; + height: 100%; + overflow: auto; + padding-block: 1rem; + padding-inline: 54px; + width: 100%; + white-space: pre-wrap; +} diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/tsconfig.json b/examples/05-interoperability/08-converting-blocks-to-react-email/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/vite.config.ts b/examples/05-interoperability/08-converting-blocks-to-react-email/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), + } as any), + }, +})); diff --git a/packages/xl-react-email-exporter/src/index.ts b/packages/xl-react-email-exporter/src/index.ts index e69de29bb2..b1fd5b0856 100644 --- a/packages/xl-react-email-exporter/src/index.ts +++ b/packages/xl-react-email-exporter/src/index.ts @@ -0,0 +1 @@ +export * from "./react-email/index.js"; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx new file mode 100644 index 0000000000..c65f4c8430 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -0,0 +1,83 @@ +import { DefaultBlockSchema } from "@blocknote/core"; +import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; +import { Heading, Img, Link, Text } from "@react-email/components"; +import { pageBreakSchema } from "@blocknote/core"; + +export const reactEmailBlockMappingForDefaultSchema: BlockMapping< + DefaultBlockSchema & typeof pageBreakSchema.blockSchema, + any, + any, + React.ReactElement, + React.ReactElement | React.ReactElement +> = { + paragraph: (block, t) => { + return {t.transformInlineContent(block.content)}; + }, + bulletListItem: (block, t) => { + // TODO + return ( + + + {t.transformInlineContent(block.content)} + + ); + }, + numberedListItem: (block, t) => { + // TODO + return ( + + + {t.transformInlineContent(block.content)} + + ); + }, + // TODO + checkListItem: (block, t) => { + return ( + + + {t.transformInlineContent(block.content)} + + ); + }, + heading: (block, t) => { + // TODO + return ( + + {t.transformInlineContent(block.content)} + + ); + }, + + codeBlock: (block) => { + return {block.type + " not implemented"}; + }, + audio: (block) => { + // TODO + return {block.type + " not implemented"}; + }, + video: (block) => { + return {block.type + " not implemented"}; + }, + file: (block) => { + return {block.type + " not implemented"}; + }, + image: (block) => { + return ( + {block.props.caption} + ); + }, + table: (block) => { + return {block.type + " not implemented"}; + }, + quote: (block) => { + return {block.type + " not implemented"}; + }, + pageBreak: () => { + // In email, a page break can be represented as a horizontal rule + return
; + }, +}; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts b/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts new file mode 100644 index 0000000000..320c91202e --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts @@ -0,0 +1,9 @@ +import { reactEmailBlockMappingForDefaultSchema } from "./blocks.js"; +import { reactEmailInlineContentMappingForDefaultSchema } from "./inlinecontent"; +import { reactEmailStyleMappingForDefaultSchema } from "./styles"; + +export const reactEmailDefaultSchemaMappings = { + blockMapping: reactEmailBlockMappingForDefaultSchema, + inlineContentMapping: reactEmailInlineContentMappingForDefaultSchema, + styleMapping: reactEmailStyleMappingForDefaultSchema, +}; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx new file mode 100644 index 0000000000..9d02c32e87 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx @@ -0,0 +1,26 @@ +import { + DefaultInlineContentSchema, + DefaultStyleSchema, + } from "@blocknote/core"; + import { InlineContentMapping } from "@blocknote/core/src/exporter/mapping.js"; + import { Link } from "@react-email/components"; + + export const reactEmailInlineContentMappingForDefaultSchema: InlineContentMapping< + DefaultInlineContentSchema, + DefaultStyleSchema, + React.ReactElement | React.ReactElement, + React.ReactElement + > = { + link: (ic, t) => { + return ( + + {...ic.content.map((content) => { + return t.transformStyledText(content); + })} + + ); + }, + text: (ic, t) => { + return t.transformStyledText(ic); + }, + }; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx new file mode 100644 index 0000000000..35ab245fc3 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx @@ -0,0 +1,63 @@ +import { DefaultStyleSchema } from "@blocknote/core"; +import { StyleMapping } from "@blocknote/core/src/exporter/mapping.js"; +import { CSSProperties } from "react"; + +export const reactEmailStyleMappingForDefaultSchema: StyleMapping< + DefaultStyleSchema, + CSSProperties +> = { + bold: (val) => { + if (!val) { + return {}; + } + return { + fontWeight: "bold", + }; + }, + italic: (val) => { + if (!val) { + return {}; + } + return { + fontStyle: "italic", + }; + }, + underline: (val) => { + if (!val) { + return {}; + } + return { + textDecoration: "underline", // TODO: could conflict with strike + }; + }, + strike: (val) => { + if (!val) { + return {}; + } + return { + textDecoration: "line-through", + }; + }, + backgroundColor: (val) => { + return { + backgroundColor: val, + }; + }, + textColor: (val) => { + if (!val) { + return {}; + } + return { + color: val, + }; + }, + // TODO? ? + code: (val) => { + if (!val) { + return {}; + } + return { + fontFamily: "Courier", + }; + }, +}; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/index.ts b/packages/xl-react-email-exporter/src/react-email/index.ts new file mode 100644 index 0000000000..83b1c99f52 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/index.ts @@ -0,0 +1,2 @@ +export * from "./defaultSchema/index"; +export * from "./reactEmailExporter"; \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx new file mode 100644 index 0000000000..459ac796a5 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -0,0 +1,130 @@ +import { describe, it } from "vitest"; + +import { + Body, + Button, + Container, + Head, + Hr, + Html, + Img, + Preview, + Section, + Text, +} from "@react-email/components"; +interface KoalaWelcomeEmailProps { + userFirstname: string; +} + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ""; + +export const KoalaWelcomeEmail = ({ + userFirstname, +}: KoalaWelcomeEmailProps) => ( + + + + The sales intelligence platform that helps you uncover qualified leads. + + + + Koala + Hi {userFirstname}, + + Welcome to Koala, the sales intelligence platform that helps you + uncover qualified leads and close deals faster. + +
+ +
+ + Best, +
+ The Koala team +
+
+ + 470 Noor Ave STE B #1148, South San Francisco, CA 94080 + +
+ + +); + +KoalaWelcomeEmail.PreviewProps = { + userFirstname: "Alan", +} as KoalaWelcomeEmailProps; + +export default KoalaWelcomeEmail; + +const main = { + backgroundColor: "#ffffff", + fontFamily: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', +}; + +const container = { + margin: "0 auto", + padding: "20px 0 48px", +}; + +const logo = { + margin: "0 auto", +}; + +const paragraph = { + fontSize: "16px", + lineHeight: "26px", +}; + +const btnContainer = { + textAlign: "center" as const, +}; + +const button = { + backgroundColor: "#5F51E8", + borderRadius: "3px", + color: "#fff", + fontSize: "16px", + textDecoration: "none", + textAlign: "center" as const, + display: "block", + padding: "12px", +}; + +const hr = { + borderColor: "#cccccc", + margin: "20px 0", +}; + +const footer = { + color: "#8898aa", + fontSize: "12px", +}; + +describe("react email exporter", () => { + it("should export a document", async () => { + // const exporter = new ReactEmailExporter( + // BlockNoteSchema.create(), + // reactEmailDefaultSchemaMappings + // ); + + //exporter.toReactEmailDocument(testDocument); + + // const html = await render(doc); + // eslint-disable-next-line no-console + // console.log(html); + // const buffer = await Packer.toBuffer(doc); + // fs.writeFileSync(__dirname + "/My Document.docx", buffer); + }); +}); \ No newline at end of file diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx new file mode 100644 index 0000000000..df2bf3645d --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -0,0 +1,136 @@ +import { + Block, + BlockNoteSchema, + BlockSchema, + COLORS_DEFAULT, + InlineContentSchema, + StyleSchema, + StyledText, + } from "@blocknote/core"; + + import { Exporter, ExporterOptions } from "@blocknote/core"; + + import { + Body, + Container, + Font, + Head, + Html, + Link, + Section, + } from "@react-email/components"; +import React from "react"; + import { CSSProperties } from "react"; + + export class ReactEmailExporter< + B extends BlockSchema, + S extends StyleSchema, + I extends InlineContentSchema + > extends Exporter< + B, + I, + S, + React.ReactElement, + React.ReactElement | React.ReactElement, + CSSProperties, + React.ReactElement + > { + public constructor( + public readonly schema: BlockNoteSchema, + mappings: Exporter< + NoInfer, + NoInfer, + NoInfer, + React.ReactElement, + React.ReactElement | React.ReactElement, + CSSProperties, + React.ReactElement + >["mappings"], + options?: Partial + ) { + const defaults = { + colors: COLORS_DEFAULT, + } satisfies Partial; + + const newOptions = { + ...defaults, + ...options, + }; + super(schema, mappings, newOptions); + } + + public transformStyledText(styledText: StyledText) { + const stylesArray = this.mapStyles(styledText.styles); + const styles = Object.assign({}, ...stylesArray); + return {styledText.text}; + } + + public transformBlocks( + blocks: Block[], // Or BlockFromConfig? + nestingLevel = 0 + ): React.ReactElement[] { + return blocks.map((b) => { + const children = this.transformBlocks(b.children, nestingLevel + 1); + const self = this.mapBlock(b as any, nestingLevel, 0) as any; // TODO: any + return ( + + {self} + {children.length > 0 && ( +
{children}
+ )} +
+ ); + }); + } + + public renderFonts() { + return ( + <> + + + + + ); + } + public toReactEmailDocument(blocks: Block[]) { + // this.registerFonts(); + return ( + + {this.renderFonts()} + {/* + TODO + */} + + {this.transformBlocks(blocks)} + + + ); + } + } \ No newline at end of file diff --git a/playground/package.json b/playground/package.json index 2f33360186..fc177a4382 100644 --- a/playground/package.json +++ b/playground/package.json @@ -30,6 +30,7 @@ "@blocknote/xl-multi-column": "workspace:^", "@blocknote/xl-odt-exporter": "workspace:^", "@blocknote/xl-pdf-exporter": "workspace:^", + "@blocknote/xl-react-email-exporter": "workspace:^", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@liveblocks/core": "^2.23.1", @@ -81,4 +82,4 @@ "../.eslintrc.json" ] } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6531e52b5e..44d87bd851 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2104,6 +2104,46 @@ importers: specifier: ^5.3.4 version: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) + examples/05-interoperability/08-converting-blocks-to-react-email: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + '@blocknote/xl-react-email-exporter': + specifier: latest + version: link:../../../packages/xl-react-email-exporter + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.0.25 + version: 18.3.20 + '@types/react-dom': + specifier: ^18.0.9 + version: 18.3.5(@types/react@18.3.20) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.4.1(vite@5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2)) + vite: + specifier: ^5.3.4 + version: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) + examples/06-custom-schema/01-alert-block: dependencies: '@blocknote/ariakit': @@ -4256,6 +4296,9 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:^ version: link:../packages/xl-pdf-exporter + '@blocknote/xl-react-email-exporter': + specifier: workspace:^ + version: link:../packages/xl-react-email-exporter '@emotion/react': specifier: ^11.11.4 version: 11.14.0(@types/react@18.3.20)(react@18.3.1) @@ -20360,6 +20403,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@4.4.1(vite@5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2))': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.3)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.10 From c12073960b31d130f386554cd13e72a42b02ca83 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 11:39:13 -0500 Subject: [PATCH 03/38] Adjusting the playground layout --- .../App.tsx | 2 +- .../index.html | 29 ++++++++-------- .../package.json | 10 +++--- .../styles.css | 33 +++++++++++-------- playground/src/examples.gen.tsx | 22 +++++++++++++ pnpm-lock.yaml | 6 ++++ 6 files changed, 66 insertions(+), 36 deletions(-) diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx index a779b62001..75aaa99171 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx @@ -352,7 +352,7 @@ export default function App() { /> -
Email Preview Here
+
); } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/index.html b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html index 86659f0b5c..b412d92142 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/index.html +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/index.html @@ -1,17 +1,14 @@ - - - - - - Exporting documents to React Email - - - -
- - - - \ No newline at end of file + + + + + Exporting documents to React Email + + +
+ + + diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index 4d939c63a1..ddc3efb176 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -1,5 +1,5 @@ { - "name": "@blocknote/example-interoperability-converting-react-email", + "name": "@blocknote/example-interoperability-converting-blocks-to-react-email", "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", "private": true, "version": "0.12.4", @@ -10,14 +10,14 @@ "preview": "vite preview" }, "dependencies": { - "@blocknote/core": "latest", - "@blocknote/react": "latest", "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", "@blocknote/mantine": "latest", + "@blocknote/react": "latest", "@blocknote/shadcn": "latest", + "@blocknote/xl-react-email-exporter": "latest", "react": "^18.3.1", - "react-dom": "^18.3.1", - "@blocknote/xl-react-email-exporter": "latest" + "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.0.25", diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css b/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css index 6d5eeba7fe..fca66829c2 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/styles.css @@ -1,25 +1,30 @@ .wrapper { display: flex; - flex-direction: column; + flex-direction: row; height: 100%; } -.item { - border-radius: 0.5rem; +@media (max-width: 800px) { + .wrapper { + flex-direction: column; + } + + .editor { + max-height: 500px; + overflow-y: scroll; + } +} + +.wrapper > div { flex: 1; - overflow: hidden; } -.item.bordered { - border: 1px solid gray; +.email { + min-height: 500px; + display: flex; + align-items: stretch; } -.item pre { - border-radius: 0.5rem; - height: 100%; - overflow: auto; - padding-block: 1rem; - padding-inline: 54px; - width: 100%; - white-space: pre-wrap; +.editor.bordered { + border: 1px solid gray; } diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 597bf4679f..a3d511bbef 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1018,6 +1018,28 @@ "pathFromRoot": "examples/05-interoperability", "slug": "interoperability" } + }, + { + "projectSlug": "converting-blocks-to-react-email", + "fullSlug": "interoperability/converting-blocks-to-react-email", + "pathFromRoot": "examples/05-interoperability/08-converting-blocks-to-react-email", + "config": { + "playground": true, + "docs": true, + "author": "jmarbutt", + "tags": [ + "" + ], + "dependencies": { + "@blocknote/xl-react-email-exporter": "latest" + } as any, + "pro": true + }, + "title": "Exporting documents to React Email", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } } ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44d87bd851..d465c70f43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2124,6 +2124,12 @@ importers: '@blocknote/xl-react-email-exporter': specifier: latest version: link:../../../packages/xl-react-email-exporter + '@react-email/components': + specifier: ^0.1.0 + version: 0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/render': + specifier: 1.1.2 + version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 From 9c322780e8cff403b78bda8a1fed5defc0f9eb45 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 13:13:51 -0500 Subject: [PATCH 04/38] Configuring Playground to Render HTML from Exporter --- .../.bnexample.json | 3 ++- .../App.tsx | 12 +++++---- .../package.json | 9 ++++--- .../react-email/reactEmailExporter.test.tsx | 17 +++++++----- .../src/react-email/reactEmailExporter.tsx | 26 +++++++++++-------- playground/src/examples.gen.tsx | 3 ++- playground/vite.config.ts | 4 +++ pnpm-lock.yaml | 5 +--- 8 files changed, 46 insertions(+), 33 deletions(-) diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json index 02fc1193cc..2ffb362d31 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json @@ -6,7 +6,8 @@ "" ], "dependencies": { - "@blocknote/xl-react-email-exporter": "latest" + "@blocknote/xl-react-email-exporter": "latest", + "@react-email/render": "^1.1.2" }, "pro": true } \ No newline at end of file diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx index 75aaa99171..a524837763 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx @@ -316,15 +316,14 @@ export default function App() { }); const onChange = async () => { + if (!editor || !editor.document) return; const exporter = new ReactEmailExporter( editor.schema, reactEmailDefaultSchemaMappings, ); - // Converts the editor's contents from Block objects to HTML and store to state. - const emailDocument = await exporter.toReactEmailDocument(editor.document); - setEmailDocument(emailDocument); + const emailHtml = await exporter.toReactEmailDocument(editor.document); - // const blob = await ReactPDF.pdf(pdfDocument).toBlob(); + setEmailDocument(emailHtml); }; useEffect(() => { @@ -352,7 +351,10 @@ export default function App() { /> -
+
); } diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index ddc3efb176..094c2f5672 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -10,14 +10,15 @@ "preview": "vite preview" }, "dependencies": { - "@blocknote/ariakit": "latest", "@blocknote/core": "latest", - "@blocknote/mantine": "latest", "@blocknote/react": "latest", + "@blocknote/ariakit": "latest", + "@blocknote/mantine": "latest", "@blocknote/shadcn": "latest", - "@blocknote/xl-react-email-exporter": "latest", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "@blocknote/xl-react-email-exporter": "latest", + "@react-email/render": "^1.1.2" }, "devDependencies": { "@types/react": "^18.0.25", diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index 459ac796a5..8b46b45d57 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -12,6 +12,10 @@ import { Section, Text, } from "@react-email/components"; +import { ReactEmailExporter } from "./reactEmailExporter"; +import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; +import { BlockNoteSchema } from "@blocknote/core"; +import { testDocument } from "@shared/testDocument.js"; interface KoalaWelcomeEmailProps { userFirstname: string; } @@ -114,16 +118,15 @@ const footer = { describe("react email exporter", () => { it("should export a document", async () => { - // const exporter = new ReactEmailExporter( - // BlockNoteSchema.create(), - // reactEmailDefaultSchemaMappings - // ); + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings + ); - //exporter.toReactEmailDocument(testDocument); + const html = await exporter.toReactEmailDocument(testDocument as any); - // const html = await render(doc); // eslint-disable-next-line no-console - // console.log(html); + console.log(html); // const buffer = await Packer.toBuffer(doc); // fs.writeFileSync(__dirname + "/My Document.docx", buffer); }); diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index df2bf3645d..1a46396388 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -18,7 +18,10 @@ import { Html, Link, Section, + Text } from "@react-email/components"; + import { pretty, render as renderEmail } from "@react-email/render"; + import React from "react"; import { CSSProperties } from "react"; @@ -119,18 +122,19 @@ import React from "react"; ); } - public toReactEmailDocument(blocks: Block[]) { - // this.registerFonts(); - return ( + public async toReactEmailDocument(blocks: Block[]) { + + return renderEmail( - {this.renderFonts()} - {/* - TODO - */} - - {this.transformBlocks(blocks)} - - + {this.renderFonts()} + {/* + TODO + */} + + Test + {/* {this.transformBlocks(blocks)} */} + + ); } } \ No newline at end of file diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index a3d511bbef..399008e871 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1031,7 +1031,8 @@ "" ], "dependencies": { - "@blocknote/xl-react-email-exporter": "latest" + "@blocknote/xl-react-email-exporter": "latest", + "@react-email/render": "^1.1.2" } as any, "pro": true }, diff --git a/playground/vite.config.ts b/playground/vite.config.ts index bdf0f7f7fb..3c4bfdae03 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -60,6 +60,10 @@ export default defineConfig((conf) => ({ __dirname, "../../liveblocks/packages/liveblocks-react-blocknote/src/", ), + "@blocknote/xl-react-email-exporter": resolve( + __dirname, + "../packages/xl-react-email-exporter/src", + ), /* This can be used when developing against a local version of liveblocks: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d465c70f43..c85ff85d78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2124,11 +2124,8 @@ importers: '@blocknote/xl-react-email-exporter': specifier: latest version: link:../../../packages/xl-react-email-exporter - '@react-email/components': - specifier: ^0.1.0 - version: 0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-email/render': - specifier: 1.1.2 + specifier: ^1.1.2 version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 From 5fecf7163ffb48fb68874ad0a56662babe970f90 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 13:26:36 -0500 Subject: [PATCH 05/38] Getting initial Rendering back on track --- .../src/react-email/defaultSchema/blocks.tsx | 2 +- .../src/react-email/reactEmailExporter.tsx | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index c65f4c8430..25aef239c8 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -10,7 +10,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< React.ReactElement, React.ReactElement | React.ReactElement > = { - paragraph: (block, t) => { + paragraph: (block, t) => { return {t.transformInlineContent(block.content)}; }, bulletListItem: (block, t) => { diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 1a46396388..a1bc33315b 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -25,7 +25,7 @@ import { import React from "react"; import { CSSProperties } from "react"; - export class ReactEmailExporter< +export class ReactEmailExporter< B extends BlockSchema, S extends StyleSchema, I extends InlineContentSchema @@ -68,13 +68,14 @@ import React from "react"; return {styledText.text}; } - public transformBlocks( + public async transformBlocks( blocks: Block[], // Or BlockFromConfig? nestingLevel = 0 - ): React.ReactElement[] { - return blocks.map((b) => { - const children = this.transformBlocks(b.children, nestingLevel + 1); - const self = this.mapBlock(b as any, nestingLevel, 0) as any; // TODO: any + ): Promise[]> { + return Promise.all(blocks.map(async (b) => { + const children = await this.transformBlocks(b.children, nestingLevel + 1); + const self = await this.mapBlock(b as any, nestingLevel, 0) as any; // TODO: any + console.log('self', self); return ( {self} @@ -83,7 +84,7 @@ import React from "react"; )} ); - }); + })); } public renderFonts() { @@ -124,6 +125,7 @@ import React from "react"; } public async toReactEmailDocument(blocks: Block[]) { + const transformedBlocks = await this.transformBlocks(blocks); return renderEmail( {this.renderFonts()} @@ -131,8 +133,8 @@ import React from "react"; TODO */} - Test - {/* {this.transformBlocks(blocks)} */} + + {transformedBlocks} ); From a31cc29fef6a19a3466b8b23c6a3bbd8769222f2 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 13:51:05 -0500 Subject: [PATCH 06/38] Adding Code Block --- .../src/react-email/defaultSchema/blocks.tsx | 38 ++++++++++++------- .../src/react-email/reactEmailExporter.tsx | 6 ++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 25aef239c8..0c5f14ec08 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -1,6 +1,6 @@ -import { DefaultBlockSchema } from "@blocknote/core"; +import { DefaultBlockSchema, StyledText } from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; -import { Heading, Img, Link, Text } from "@react-email/components"; +import { CodeBlock, CodeInline, dracula, Font, Heading, Img, Link, Text,} from "@react-email/components"; import { pageBreakSchema } from "@blocknote/core"; export const reactEmailBlockMappingForDefaultSchema: BlockMapping< @@ -14,21 +14,23 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< return {t.transformInlineContent(block.content)}; }, bulletListItem: (block, t) => { - // TODO + // Use
    and
  • with Tailwind classes via className (supported by react-email) return ( - - - {t.transformInlineContent(block.content)} - +
      +
    • + {t.transformInlineContent(block.content)} +
    • +
    ); }, - numberedListItem: (block, t) => { - // TODO + numberedListItem: (block, t, _nestingLevel, numberedListIndex) => { + // Use
      and
    1. with Tailwind classes via className (supported by react-email) return ( - - - {t.transformInlineContent(block.content)} - +
        +
      1. + {t.transformInlineContent(block.content)} +
      2. +
      ); }, // TODO @@ -50,7 +52,15 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< }, codeBlock: (block) => { - return {block.type + " not implemented"}; + const textContent = (block.content as StyledText[])[0]?.text || ""; + + return + }, audio: (block) => { // TODO diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index a1bc33315b..6a32810562 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -18,6 +18,7 @@ import { Html, Link, Section, + Tailwind, Text } from "@react-email/components"; import { pretty, render as renderEmail } from "@react-email/render"; @@ -133,8 +134,9 @@ export class ReactEmailExporter< TODO */} - - {transformedBlocks} + + {transformedBlocks} + ); From 2b58db6ea096663f9159e5628bd9f818ce873709 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:01:27 -0500 Subject: [PATCH 07/38] Setting up Numbered list items --- .../src/react-email/defaultSchema/blocks.tsx | 4 +-- .../src/react-email/reactEmailExporter.tsx | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 0c5f14ec08..b4666a5e07 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -26,9 +26,9 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< numberedListItem: (block, t, _nestingLevel, numberedListIndex) => { // Use
        and
      1. with Tailwind classes via className (supported by react-email) return ( -
          +
          1. - {t.transformInlineContent(block.content)} + {t.transformInlineContent(block.content)}
          ); diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 6a32810562..e8585ebba5 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -73,19 +73,33 @@ export class ReactEmailExporter< blocks: Block[], // Or BlockFromConfig? nestingLevel = 0 ): Promise[]> { - return Promise.all(blocks.map(async (b) => { + + const ret: React.ReactElement[] = []; + let numberedListIndex = 0; + + for (const b of blocks) { + + if (b.type === "numberedListItem") { + numberedListIndex++; + } else { + numberedListIndex = 0; + } + + const children = await this.transformBlocks(b.children, nestingLevel + 1); - const self = await this.mapBlock(b as any, nestingLevel, 0) as any; // TODO: any - console.log('self', self); - return ( - + const self = await this.mapBlock(b as any, nestingLevel, numberedListIndex) as any; // TODO: any + + ret.push( + {self} {children.length > 0 && (
          {children}
          )}
          ); - })); + } + + return ret; } public renderFonts() { From f1987d703459d997721287e0a9ca528822c1f60a Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:04:05 -0500 Subject: [PATCH 08/38] Hiding Audio, Video, File from Email rendering --- .../src/react-email/defaultSchema/blocks.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index b4666a5e07..9a84b20ffd 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -62,15 +62,14 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< /> }, - audio: (block) => { - // TODO - return {block.type + " not implemented"}; + audio: () => { + return <>; // Audio blocks are not typically rendered in email }, - video: (block) => { - return {block.type + " not implemented"}; + video: () => { + return <>; // Video blocks are not typically rendered in email }, - file: (block) => { - return {block.type + " not implemented"}; + file: () => { + return <>; // File blocks are not typically rendered in email }, image: (block) => { return ( From 72abd035a2919826b6ac5b685860c3799a72e44e Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:09:11 -0500 Subject: [PATCH 09/38] Setting Styles --- .../src/react-email/reactEmailExporter.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index e8585ebba5..1eb16587b1 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -3,6 +3,7 @@ import { BlockNoteSchema, BlockSchema, COLORS_DEFAULT, + DefaultProps, InlineContentSchema, StyleSchema, StyledText, @@ -89,11 +90,13 @@ export class ReactEmailExporter< const children = await this.transformBlocks(b.children, nestingLevel + 1); const self = await this.mapBlock(b as any, nestingLevel, numberedListIndex) as any; // TODO: any + const style = this.blocknoteDefaultPropsToReactEmailStyle(b.props as any); + ret.push( - {self} +
          {self}
          {children.length > 0 && ( -
          {children}
          +
          {children}
          )}
          ); @@ -155,4 +158,31 @@ export class ReactEmailExporter< ); } + + + protected blocknoteDefaultPropsToReactEmailStyle( + props: Partial, + ): any { + return { + textAlign: props.textAlignment, + backgroundColor: + props.backgroundColor === "default" || !props.backgroundColor + ? undefined + : this.options.colors[ + props.backgroundColor as keyof typeof this.options.colors + ].background, + color: + props.textColor === "default" || !props.textColor + ? undefined + : this.options.colors[ + props.textColor as keyof typeof this.options.colors + ].text, + alignItems: + props.textAlignment === "right" + ? "flex-end" + : props.textAlignment === "center" + ? "center" + : undefined, + }; + } } \ No newline at end of file From 30e44c0d66100cf1d996ec31004754fbb085927b Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:12:21 -0500 Subject: [PATCH 10/38] Adding Checkbox rendering --- .../src/react-email/defaultSchema/blocks.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 9a84b20ffd..e8e7671fd9 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -35,15 +35,27 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< }, // TODO checkListItem: (block, t) => { + // Render a checkbox using inline SVG for better appearance in email + // block.props.checked should be true/false + const checked = block.props?.checked; + const checkboxSvg = checked ? ( + + + + + ) : ( + + + + ); return ( - - {t.transformInlineContent(block.content)} + {checkboxSvg} + {t.transformInlineContent(block.content)} ); }, - heading: (block, t) => { - // TODO + heading: (block, t) => { return ( {t.transformInlineContent(block.content)} From 635972e23969139cb802197cdad1249cde05f744 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:27:29 -0500 Subject: [PATCH 11/38] Implementing Tables --- .../src/react-email/defaultSchema/blocks.tsx | 52 +++++++++++++++++-- .../src/react-email/defaultSchema/styles.tsx | 3 +- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index e8e7671fd9..c652f89986 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -1,6 +1,7 @@ import { DefaultBlockSchema, StyledText } from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; -import { CodeBlock, CodeInline, dracula, Font, Heading, Img, Link, Text,} from "@react-email/components"; +import { CodeBlock, dracula, Heading, Img, Link, Text,} from "@react-email/components"; +import { Section, Row, Column } from "@react-email/components"; import { pageBreakSchema } from "@blocknote/core"; export const reactEmailBlockMappingForDefaultSchema: BlockMapping< @@ -91,8 +92,53 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< alt={block.props.caption} /> ); }, - table: (block) => { - return {block.type + " not implemented"}; + table: (block, t) => { + + // Render table using react-email Section, Row, and Column components + // See: https://react.email/docs/components/section + const table = block.content; + if (!table || typeof table !== 'object' || !Array.isArray(table.rows)) { + return Table data not available; + } + const headerRows = new Array((table.headerRows as number) ?? 0).fill(true); + const headerCols = new Array((table.headerCols as number) ?? 0).fill(true); + const columnWidths = (table.columnWidths as number[] | undefined) || []; + // Calculate number of columns from the first row + const numCols = table.rows[0]?.cells?.length || 1; + // If no explicit columnWidths, use equal percentage widths + const defaultColWidth = `${(100 / numCols).toFixed(2)}%`; + + return ( +
          + {table.rows.map((row: any, rowIndex: any) => ( + + {row.cells.map((cell: any, colIndex: any) => { + const isHeaderRow = headerRows[rowIndex]; + const isHeaderCol = headerCols[colIndex]; + const isHeader = isHeaderRow || isHeaderCol; + // Use explicit width if provided, else fallback to equal width + const width = columnWidths[colIndex] ? columnWidths[colIndex] : defaultColWidth; + return ( + + {t.transformInlineContent(cell.content)} + + ); + })} + + ))} +
          + ); }, quote: (block) => { return {block.type + " not implemented"}; diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx index 35ab245fc3..b7a5ea110d 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx @@ -50,8 +50,7 @@ export const reactEmailStyleMappingForDefaultSchema: StyleMapping< return { color: val, }; - }, - // TODO? ? + }, code: (val) => { if (!val) { return {}; From 77779770127c7f4f23fc41ff24afb0b3d0b89922 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:33:51 -0500 Subject: [PATCH 12/38] Implementing Block Quote --- .../src/react-email/defaultSchema/blocks.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index c652f89986..20a5a20b37 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -140,8 +140,23 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< ); }, - quote: (block) => { - return {block.type + " not implemented"}; + quote: (block, t) => { + // Render block quote with a left border and subtle background for email compatibility + return ( + + {t.transformInlineContent(block.content)} + + ); }, pageBreak: () => { // In email, a page break can be represented as a horizontal rule From 5b8a8a0b05f3efbe1197077aea585a7224df9e7e Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Wed, 18 Jun 2025 14:35:57 -0500 Subject: [PATCH 13/38] Removing Todo --- .../src/react-email/defaultSchema/blocks.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 20a5a20b37..5f4baa3c98 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -33,8 +33,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping<
        ); - }, - // TODO + }, checkListItem: (block, t) => { // Render a checkbox using inline SVG for better appearance in email // block.props.checked should be true/false From 3a4c0a4a1f4d58c2eefba34c47916371fad6780c Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 09:33:04 -0500 Subject: [PATCH 14/38] Removing Page Break from demo, Adding Icons and Downloads for audio, video, files --- .../App.tsx | 5 +- .../src/react-email/defaultSchema/blocks.tsx | 71 ++++++++++++++++--- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx index a524837763..18dcef6feb 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx @@ -332,10 +332,7 @@ export default function App() { }, []); const slashMenuItems = useMemo(() => { - return combineByGroup( - getDefaultReactSlashMenuItems(editor), - getPageBreakReactSlashMenuItems(editor), - ); + return combineByGroup(getDefaultReactSlashMenuItems(editor)); }, [editor]); // Renders the editor instance, and its contents as HTML below. diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 5f4baa3c98..5fefa9a739 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -1,6 +1,6 @@ import { DefaultBlockSchema, StyledText } from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; -import { CodeBlock, dracula, Heading, Img, Link, Text,} from "@react-email/components"; +import { CodeBlock, dracula, Heading, Img, Link, PrismLanguage, Text,} from "@react-email/components"; import { Section, Row, Column } from "@react-email/components"; import { pageBreakSchema } from "@blocknote/core"; @@ -69,19 +69,55 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< return }, - audio: () => { - return <>; // Audio blocks are not typically rendered in email + audio: (block) => { + // Audio icon SVG + const icon = ( + + + + ); + const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; + return ( +
        + + +
        + ); }, - video: () => { - return <>; // Video blocks are not typically rendered in email + video: (block) => { + // Video icon SVG + const icon = ( + + + + ); + const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; + return ( +
        + + +
        + ); }, - file: () => { - return <>; // File blocks are not typically rendered in email + file: (block) => { + // File icon SVG + const icon = ( + + + + ); + const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; + return ( +
        + + +
        + ); }, image: (block) => { return ( @@ -161,4 +197,21 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< // In email, a page break can be represented as a horizontal rule return
        ; }, -}; \ No newline at end of file +}; + +// Helper for file-like blocks (audio, video, file) +function FileLink({ url, name, defaultText, icon }: { url?: string, name?: string, defaultText: string, icon: React.ReactElement }) { + return ( + + {icon} + {name || defaultText} + + ); +} + +function Caption({ caption, width }: { caption?: string, width?: number }) { + if (!caption) return null; + return ( + {caption} + ); +} \ No newline at end of file From 0f092bcee8c6d513310fbad020113e7a8edb7040 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 09:42:47 -0500 Subject: [PATCH 15/38] Using a traditional HTML table in Email Render instead of react-email section --- .../src/react-email/defaultSchema/blocks.tsx | 75 ++++++++----------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 5fefa9a739..23b1c28a13 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -1,7 +1,6 @@ import { DefaultBlockSchema, StyledText } from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; import { CodeBlock, dracula, Heading, Img, Link, PrismLanguage, Text,} from "@react-email/components"; -import { Section, Row, Column } from "@react-email/components"; import { pageBreakSchema } from "@blocknote/core"; export const reactEmailBlockMappingForDefaultSchema: BlockMapping< @@ -127,52 +126,44 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< alt={block.props.caption} /> ); }, - table: (block, t) => { - - // Render table using react-email Section, Row, and Column components - // See: https://react.email/docs/components/section + table: (block, t) => { + // Render table using standard HTML table elements for email compatibility const table = block.content; if (!table || typeof table !== 'object' || !Array.isArray(table.rows)) { return Table data not available; } - const headerRows = new Array((table.headerRows as number) ?? 0).fill(true); - const headerCols = new Array((table.headerCols as number) ?? 0).fill(true); - const columnWidths = (table.columnWidths as number[] | undefined) || []; - // Calculate number of columns from the first row - const numCols = table.rows[0]?.cells?.length || 1; - // If no explicit columnWidths, use equal percentage widths - const defaultColWidth = `${(100 / numCols).toFixed(2)}%`; - + const headerRowsCount = (table.headerRows as number) ?? 0; + const headerColsCount = (table.headerCols as number) ?? 0; + return ( -
        - {table.rows.map((row: any, rowIndex: any) => ( - - {row.cells.map((cell: any, colIndex: any) => { - const isHeaderRow = headerRows[rowIndex]; - const isHeaderCol = headerCols[colIndex]; - const isHeader = isHeaderRow || isHeaderCol; - // Use explicit width if provided, else fallback to equal width - const width = columnWidths[colIndex] ? columnWidths[colIndex] : defaultColWidth; - return ( - - {t.transformInlineContent(cell.content)} - - ); - })} - - ))} -
        + + + {table.rows.map((row: any, rowIndex: number) => ( + + {row.cells.map((cell: any, colIndex: number) => { + const isHeaderRow = rowIndex < headerRowsCount; + const isHeaderCol = colIndex < headerColsCount; + const isHeader = isHeaderRow || isHeaderCol; + const CellTag = isHeader ? 'th' : 'td'; + return ( + + {t.transformInlineContent(cell.content)} + + ); + })} + + ))} + +
        ); }, quote: (block, t) => { From f5c5a631ab277af2774e55a9ed998e7b7191a8c3 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 09:59:11 -0500 Subject: [PATCH 16/38] Working on Nested Lists tracking --- .../src/react-email/defaultSchema/blocks.tsx | 22 ++-- .../src/react-email/reactEmailExporter.tsx | 124 ++++++++++++++---- 2 files changed, 111 insertions(+), 35 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 23b1c28a13..9fcd745518 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -14,23 +14,19 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< return {t.transformInlineContent(block.content)}; }, bulletListItem: (block, t) => { - // Use
          and
        • with Tailwind classes via className (supported by react-email) + // Return only the
        • for grouping in the exporter return ( -
            -
          • - {t.transformInlineContent(block.content)} -
          • -
          +
        • + {t.transformInlineContent(block.content)} +
        • ); }, - numberedListItem: (block, t, _nestingLevel, numberedListIndex) => { - // Use
            and
          1. with Tailwind classes via className (supported by react-email) + numberedListItem: (block, t, _nestingLevel) => { + // Return only the
          2. for grouping in the exporter return ( -
              -
            1. - {t.transformInlineContent(block.content)} -
            2. -
            +
          3. + {t.transformInlineContent(block.content)} +
          4. ); }, checkListItem: (block, t) => { diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 1eb16587b1..f1481aaa39 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -19,10 +19,8 @@ import { Html, Link, Section, - Tailwind, - Text - } from "@react-email/components"; - import { pretty, render as renderEmail } from "@react-email/render"; + Tailwind } from "@react-email/components"; + import { render as renderEmail } from "@react-email/render"; import React from "react"; import { CSSProperties } from "react"; @@ -71,27 +69,109 @@ export class ReactEmailExporter< } public async transformBlocks( - blocks: Block[], // Or BlockFromConfig? + blocks: Block[], nestingLevel = 0 - ): Promise[]> { - - const ret: React.ReactElement[] = []; - let numberedListIndex = 0; - - for (const b of blocks) { - - if (b.type === "numberedListItem") { - numberedListIndex++; - } else { - numberedListIndex = 0; + ): Promise[]> { + const ret: React.ReactElement[] = []; + let i = 0; + while (i < blocks.length) { + const b = blocks[i]; + // Group only consecutive list items of the same type + if (b.type === "bulletListItem" || b.type === "numberedListItem") { + const listType = b.type; + const listItems: React.ReactElement[] = []; + let j = i; + let startIndex = 1; + while ( + j < blocks.length && + blocks[j].type === listType // Only group same-type + ) { + const block = blocks[j]; + const liContent = await this.mapBlock(block as any, nestingLevel, startIndex) as any; + let nestedList: React.ReactElement[] = []; + if (block.children && block.children.length > 0) { + // Group children by consecutive list type and render each group + let k = 0; + while (k < block.children.length) { + const child = block.children[k]; + if (child.type === "bulletListItem" || child.type === "numberedListItem") { + const childListType = child.type; + const childListItems: React.ReactElement[] = []; + let l = k; + let childStartIndex = 1; + while ( + l < block.children.length && + block.children[l].type === childListType // Only group same-type + ) { + const childBlock = block.children[l]; + const childLiContent = await this.mapBlock(childBlock as any, nestingLevel + 1, childStartIndex) as any; + let childNestedList: React.ReactElement[] = []; + if (childBlock.children && childBlock.children.length > 0) { + const grouped = await this.transformBlocks(childBlock.children, nestingLevel + 2); + childNestedList = grouped; + } + childListItems.push( + + {childLiContent} + {childNestedList.length > 0 && childNestedList} + + ); + l++; + childStartIndex++; + } + // Wrap in correct list type + if (childListType === "bulletListItem") { + nestedList.push( +
              + {childListItems} +
            + ); + } else { + nestedList.push( +
              + {childListItems} +
            + ); + } + k = l; + } else { + // Non-list child, render as normal + const childBlocks = await this.transformBlocks([child], nestingLevel + 1); + nestedList.push(...childBlocks); + k++; + } + } + } + listItems.push( + + {liContent} + {nestedList.length > 0 && nestedList} + + ); + j++; + startIndex++; + } + // Wrap in
              or
                + if (listType === "bulletListItem") { + ret.push( +
                  + {listItems} +
                + ); + } else { + ret.push( +
                  + {listItems} +
                + ); + } + i = j; + continue; } - - + // Non-list blocks const children = await this.transformBlocks(b.children, nestingLevel + 1); - const self = await this.mapBlock(b as any, nestingLevel, numberedListIndex) as any; // TODO: any - + const self = await this.mapBlock(b as any, nestingLevel, 0) as any; const style = this.blocknoteDefaultPropsToReactEmailStyle(b.props as any); - ret.push(
                {self}
                @@ -100,8 +180,8 @@ export class ReactEmailExporter< )}
                ); + i++; } - return ret; } From 3396e08d97eb5ebebc27e805c2b018ab7c157222 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 10:01:59 -0500 Subject: [PATCH 17/38] Cleaning up the list logic into its own function --- .../src/react-email/reactEmailExporter.tsx | 184 ++++++++++-------- 1 file changed, 100 insertions(+), 84 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index f1481aaa39..b2c3c22fff 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -68,104 +68,120 @@ export class ReactEmailExporter< return {styledText.text}; } - public async transformBlocks( + private async renderGroupedListBlocks( blocks: Block[], - nestingLevel = 0 + startIndex: number, + nestingLevel: number + ): Promise<{ element: React.ReactElement, nextIndex: number }> { + const listType = blocks[startIndex].type; + const listItems: React.ReactElement[] = []; + let j = startIndex; + let itemIndex = 1; + while ( + j < blocks.length && + blocks[j].type === listType + ) { + const block = blocks[j]; + const liContent = await this.mapBlock(block as any, nestingLevel, itemIndex) as any; + let nestedList: React.ReactElement[] = []; + if (block.children && block.children.length > 0) { + nestedList = await this.renderNestedLists(block.children, nestingLevel + 1, block.id); + } + listItems.push( + + {liContent} + {nestedList.length > 0 && nestedList} + + ); + j++; + itemIndex++; + } + let element: React.ReactElement; + if (listType === "bulletListItem") { + element = ( +
                  + {listItems} +
                + ); + } else { + element = ( +
                  + {listItems} +
                + ); + } + return { element, nextIndex: j }; + } + + private async renderNestedLists( + children: Block[], + nestingLevel: number, + parentId: string ): Promise[]> { - const ret: React.ReactElement[] = []; - let i = 0; - while (i < blocks.length) { - const b = blocks[i]; - // Group only consecutive list items of the same type - if (b.type === "bulletListItem" || b.type === "numberedListItem") { - const listType = b.type; - const listItems: React.ReactElement[] = []; - let j = i; - let startIndex = 1; + const nestedList: React.ReactElement[] = []; + let k = 0; + while (k < children.length) { + const child = children[k]; + if (child.type === "bulletListItem" || child.type === "numberedListItem") { + const childListType = child.type; + const childListItems: React.ReactElement[] = []; + let l = k; + let childStartIndex = 1; while ( - j < blocks.length && - blocks[j].type === listType // Only group same-type + l < children.length && + children[l].type === childListType ) { - const block = blocks[j]; - const liContent = await this.mapBlock(block as any, nestingLevel, startIndex) as any; - let nestedList: React.ReactElement[] = []; - if (block.children && block.children.length > 0) { - // Group children by consecutive list type and render each group - let k = 0; - while (k < block.children.length) { - const child = block.children[k]; - if (child.type === "bulletListItem" || child.type === "numberedListItem") { - const childListType = child.type; - const childListItems: React.ReactElement[] = []; - let l = k; - let childStartIndex = 1; - while ( - l < block.children.length && - block.children[l].type === childListType // Only group same-type - ) { - const childBlock = block.children[l]; - const childLiContent = await this.mapBlock(childBlock as any, nestingLevel + 1, childStartIndex) as any; - let childNestedList: React.ReactElement[] = []; - if (childBlock.children && childBlock.children.length > 0) { - const grouped = await this.transformBlocks(childBlock.children, nestingLevel + 2); - childNestedList = grouped; - } - childListItems.push( - - {childLiContent} - {childNestedList.length > 0 && childNestedList} - - ); - l++; - childStartIndex++; - } - // Wrap in correct list type - if (childListType === "bulletListItem") { - nestedList.push( -
                  - {childListItems} -
                - ); - } else { - nestedList.push( -
                  - {childListItems} -
                - ); - } - k = l; - } else { - // Non-list child, render as normal - const childBlocks = await this.transformBlocks([child], nestingLevel + 1); - nestedList.push(...childBlocks); - k++; - } - } + const childBlock = children[l]; + const childLiContent = await this.mapBlock(childBlock as any, nestingLevel, childStartIndex) as any; + let childNestedList: React.ReactElement[] = []; + if (childBlock.children && childBlock.children.length > 0) { + childNestedList = await this.renderNestedLists(childBlock.children, nestingLevel + 1, childBlock.id); } - listItems.push( - - {liContent} - {nestedList.length > 0 && nestedList} + childListItems.push( + + {childLiContent} + {childNestedList.length > 0 && childNestedList} ); - j++; - startIndex++; + l++; + childStartIndex++; } - // Wrap in
                  or
                    - if (listType === "bulletListItem") { - ret.push( -
                      - {listItems} + if (childListType === "bulletListItem") { + nestedList.push( +
                        + {childListItems}
                      ); } else { - ret.push( -
                        - {listItems} + nestedList.push( +
                          + {childListItems}
                        ); } - i = j; + k = l; + } else { + // Non-list child, render as normal + const childBlocks = await this.transformBlocks([child], nestingLevel); + nestedList.push(...childBlocks); + k++; + } + } + return nestedList; + } + + public async transformBlocks( + blocks: Block[], + nestingLevel = 0 + ): Promise[]> { + const ret: React.ReactElement[] = []; + let i = 0; + while (i < blocks.length) { + const b = blocks[i]; + if (b.type === "bulletListItem" || b.type === "numberedListItem") { + const { element, nextIndex } = await this.renderGroupedListBlocks(blocks, i, nestingLevel); + ret.push(element); + i = nextIndex; continue; } // Non-list blocks From 29ca82b3d30ee74afe66294fb8909204e7a6364e Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 10:34:23 -0500 Subject: [PATCH 18/38] Resolving issu with extra items in list --- .../src/react-email/defaultSchema/blocks.tsx | 12 ++------- .../src/react-email/reactEmailExporter.tsx | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 9fcd745518..c277a9623a 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -15,19 +15,11 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< }, bulletListItem: (block, t) => { // Return only the
                      1. for grouping in the exporter - return ( -
                      2. - {t.transformInlineContent(block.content)} -
                      3. - ); + return {t.transformInlineContent(block.content)}; }, numberedListItem: (block, t, _nestingLevel) => { // Return only the
                      4. for grouping in the exporter - return ( -
                      5. - {t.transformInlineContent(block.content)} -
                      6. - ); + return {t.transformInlineContent(block.content)}; }, checkListItem: (block, t) => { // Render a checkbox using inline SVG for better appearance in email diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index b2c3c22fff..f6432dd105 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -133,15 +133,26 @@ export class ReactEmailExporter< ) { const childBlock = children[l]; const childLiContent = await this.mapBlock(childBlock as any, nestingLevel, childStartIndex) as any; - let childNestedList: React.ReactElement[] = []; + const style = this.blocknoteDefaultPropsToReactEmailStyle(childBlock.props as any); + let nestedListContent: React.ReactElement[] = []; if (childBlock.children && childBlock.children.length > 0) { - childNestedList = await this.renderNestedLists(childBlock.children, nestingLevel + 1, childBlock.id); + // Check if children are list items + if ( + childBlock.children[0] && + (childBlock.children[0].type === "bulletListItem" || childBlock.children[0].type === "numberedListItem") + ) { + // Render nested list inside this
                      7. + nestedListContent = await this.renderNestedLists(childBlock.children, nestingLevel + 1, childBlock.id); + } else { + // Render non-list children as block content inside this
                      8. + nestedListContent = await this.transformBlocks(childBlock.children, nestingLevel + 1); + } } childListItems.push( - +
                      9. {childLiContent} - {childNestedList.length > 0 && childNestedList} - + {nestedListContent.length > 0 && nestedListContent} +
                      10. ); l++; childStartIndex++; @@ -161,7 +172,7 @@ export class ReactEmailExporter< } k = l; } else { - // Non-list child, render as normal + // Non-list child, render as normal (do not wrap in Section here) const childBlocks = await this.transformBlocks([child], nestingLevel); nestedList.push(...childBlocks); k++; @@ -191,9 +202,7 @@ export class ReactEmailExporter< ret.push(
                        {self}
                        - {children.length > 0 && ( -
                        {children}
                        - )} + {children.length > 0 && children}
                        ); i++; From 8495168e543ba35cf38b81d194ebbf4368c26322 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 10:35:48 -0500 Subject: [PATCH 19/38] Simplifying the nested lists function --- .../src/react-email/reactEmailExporter.tsx | 70 +++++++++---------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index f6432dd105..820eeacdf0 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -119,63 +119,59 @@ export class ReactEmailExporter< parentId: string ): Promise[]> { const nestedList: React.ReactElement[] = []; - let k = 0; - while (k < children.length) { - const child = children[k]; + let i = 0; + while (i < children.length) { + const child = children[i]; if (child.type === "bulletListItem" || child.type === "numberedListItem") { - const childListType = child.type; - const childListItems: React.ReactElement[] = []; - let l = k; - let childStartIndex = 1; - while ( - l < children.length && - children[l].type === childListType - ) { - const childBlock = children[l]; - const childLiContent = await this.mapBlock(childBlock as any, nestingLevel, childStartIndex) as any; - const style = this.blocknoteDefaultPropsToReactEmailStyle(childBlock.props as any); - let nestedListContent: React.ReactElement[] = []; - if (childBlock.children && childBlock.children.length > 0) { - // Check if children are list items + // Group consecutive list items of the same type + const listType = child.type; + const listItems: React.ReactElement[] = []; + let j = i; + let itemIndex = 1; + while (j < children.length && children[j].type === listType) { + const listItem = children[j]; + const liContent = await this.mapBlock(listItem as any, nestingLevel, itemIndex) as any; + const style = this.blocknoteDefaultPropsToReactEmailStyle(listItem.props as any); + let nestedContent: React.ReactElement[] = []; + if (listItem.children && listItem.children.length > 0) { + // If children are list items, render as nested list; otherwise, as normal blocks if ( - childBlock.children[0] && - (childBlock.children[0].type === "bulletListItem" || childBlock.children[0].type === "numberedListItem") + listItem.children[0] && + (listItem.children[0].type === "bulletListItem" || listItem.children[0].type === "numberedListItem") ) { - // Render nested list inside this
                      11. - nestedListContent = await this.renderNestedLists(childBlock.children, nestingLevel + 1, childBlock.id); + nestedContent = await this.renderNestedLists(listItem.children, nestingLevel + 1, listItem.id); } else { - // Render non-list children as block content inside this
                      12. - nestedListContent = await this.transformBlocks(childBlock.children, nestingLevel + 1); + nestedContent = await this.transformBlocks(listItem.children, nestingLevel + 1); } } - childListItems.push( -
                      13. - {childLiContent} - {nestedListContent.length > 0 && nestedListContent} + listItems.push( +
                      14. + {liContent} + {nestedContent.length > 0 && nestedContent}
                      15. ); - l++; - childStartIndex++; + j++; + itemIndex++; } - if (childListType === "bulletListItem") { + if (listType === "bulletListItem") { nestedList.push( -
                          - {childListItems} +
                            + {listItems}
                          ); } else { nestedList.push( -
                            - {childListItems} +
                              + {listItems}
                            ); } - k = l; + i = j; } else { - // Non-list child, render as normal (do not wrap in Section here) + // Non-list child, render as normal const childBlocks = await this.transformBlocks([child], nestingLevel); nestedList.push(...childBlocks); - k++; + i++; } } return nestedList; From 77a8ccc2d20de35f8471031e8580cea1ec739d34 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 10:37:03 -0500 Subject: [PATCH 20/38] Removing unused example --- .../react-email/reactEmailExporter.test.tsx | 110 ------------------ 1 file changed, 110 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index 8b46b45d57..7cf31791a1 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,120 +1,10 @@ import { describe, it } from "vitest"; -import { - Body, - Button, - Container, - Head, - Hr, - Html, - Img, - Preview, - Section, - Text, -} from "@react-email/components"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { BlockNoteSchema } from "@blocknote/core"; import { testDocument } from "@shared/testDocument.js"; -interface KoalaWelcomeEmailProps { - userFirstname: string; -} -const baseUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : ""; - -export const KoalaWelcomeEmail = ({ - userFirstname, -}: KoalaWelcomeEmailProps) => ( - - - - The sales intelligence platform that helps you uncover qualified leads. - - - - Koala - Hi {userFirstname}, - - Welcome to Koala, the sales intelligence platform that helps you - uncover qualified leads and close deals faster. - -
                            - -
                            - - Best, -
                            - The Koala team -
                            -
                            - - 470 Noor Ave STE B #1148, South San Francisco, CA 94080 - -
                            - - -); - -KoalaWelcomeEmail.PreviewProps = { - userFirstname: "Alan", -} as KoalaWelcomeEmailProps; - -export default KoalaWelcomeEmail; - -const main = { - backgroundColor: "#ffffff", - fontFamily: - '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', -}; - -const container = { - margin: "0 auto", - padding: "20px 0 48px", -}; - -const logo = { - margin: "0 auto", -}; - -const paragraph = { - fontSize: "16px", - lineHeight: "26px", -}; - -const btnContainer = { - textAlign: "center" as const, -}; - -const button = { - backgroundColor: "#5F51E8", - borderRadius: "3px", - color: "#fff", - fontSize: "16px", - textDecoration: "none", - textAlign: "center" as const, - display: "block", - padding: "12px", -}; - -const hr = { - borderColor: "#cccccc", - margin: "20px 0", -}; - -const footer = { - color: "#8898aa", - fontSize: "12px", -}; describe("react email exporter", () => { it("should export a document", async () => { From 5d4e12627e1888af74605e0a7d6a2ae782af3524 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 10:43:45 -0500 Subject: [PATCH 21/38] Trying to get the test for the render --- .../src/react-email/reactEmailExporter.test.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index 7cf31791a1..c93e46ad36 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,5 +1,5 @@ -import { describe, it } from "vitest"; - +import { describe, it, expect } from "vitest"; +import { render } from "@react-email/render"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { BlockNoteSchema } from "@blocknote/core"; @@ -7,17 +7,14 @@ import { testDocument } from "@shared/testDocument.js"; describe("react email exporter", () => { - it("should export a document", async () => { + it("should export a document (HTML snapshot)", async () => { const exporter = new ReactEmailExporter( BlockNoteSchema.create(), reactEmailDefaultSchemaMappings ); - const html = await exporter.toReactEmailDocument(testDocument as any); - - // eslint-disable-next-line no-console - console.log(html); - // const buffer = await Packer.toBuffer(doc); - // fs.writeFileSync(__dirname + "/My Document.docx", buffer); + const reactElement = await exporter.toReactEmailDocument(testDocument as any); + const html = render(reactElement); + expect(html).toMatchSnapshot(); }); }); \ No newline at end of file From 129dc4ea48e0dcd2bbf1dbeb8ac4a4de0de5e49f Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 11:06:28 -0500 Subject: [PATCH 22/38] Removing Google Fonts --- .../src/react-email/reactEmailExporter.tsx | 62 ++++--------------- 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 820eeacdf0..0a2f309d0b 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -14,7 +14,6 @@ import { import { Body, Container, - Font, Head, Html, Link, @@ -205,58 +204,23 @@ export class ReactEmailExporter< } return ret; } - - public renderFonts() { - return ( - <> - - - - - ); - } - public async toReactEmailDocument(blocks: Block[]) { + public async toReactEmailDocument(blocks: Block[]) { const transformedBlocks = await this.transformBlocks(blocks); return renderEmail( - {this.renderFonts()} - {/* - TODO - */} - - - {transformedBlocks} - - - + + + + {transformedBlocks} + + + ); } From 3bca7876182e16926c5af62aeeaef8e249fc7b6d Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 11:10:46 -0500 Subject: [PATCH 23/38] Adding email preview --- .../src/react-email/reactEmailExporter.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 0a2f309d0b..690fd125eb 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -17,6 +17,7 @@ import { Head, Html, Link, + Preview, Section, Tailwind } from "@react-email/components"; import { render as renderEmail } from "@react-email/render"; @@ -205,7 +206,12 @@ export class ReactEmailExporter< return ret; } - public async toReactEmailDocument(blocks: Block[]) { + public async toReactEmailDocument( + blocks: Block[], + options: { + preview?: string | string[]; + } + ) { const transformedBlocks = await this.transformBlocks(blocks); return renderEmail( @@ -216,7 +222,8 @@ export class ReactEmailExporter< lineHeight: "1.5", color: "#333", }}> - + {options.preview && {options.preview}} + {transformedBlocks} From 72e0dd43918c98c43744616ee909d04f9afd5f99 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 11:14:27 -0500 Subject: [PATCH 24/38] Fixing unit tests --- .../__snapshots__/reactEmailExporter.test.tsx.snap | 3 +++ .../src/react-email/reactEmailExporter.tsx | 4 ++-- packages/xl-react-email-exporter/vitestSetup.ts | 10 ++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap create mode 100644 packages/xl-react-email-exporter/vitestSetup.ts diff --git a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap new file mode 100644 index 0000000000..b15bc4a121 --- /dev/null +++ b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`react email exporter > should export a document (HTML snapshot) 1`] = `Promise {}`; diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 690fd125eb..6e969c794b 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -208,7 +208,7 @@ export class ReactEmailExporter< public async toReactEmailDocument( blocks: Block[], - options: { + options?: { preview?: string | string[]; } ) { @@ -222,7 +222,7 @@ export class ReactEmailExporter< lineHeight: "1.5", color: "#333", }}> - {options.preview && {options.preview}} + {options?.preview && {options.preview}} {transformedBlocks} diff --git a/packages/xl-react-email-exporter/vitestSetup.ts b/packages/xl-react-email-exporter/vitestSetup.ts new file mode 100644 index 0000000000..a946b5fc3a --- /dev/null +++ b/packages/xl-react-email-exporter/vitestSetup.ts @@ -0,0 +1,10 @@ +import { afterEach, beforeEach } from "vitest"; + +beforeEach(() => { + globalThis.window = globalThis.window || ({} as any); + (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {}; +}); + +afterEach(() => { + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; +}); From c9c2f13d0edf4ae7bf23cfb8948fd535b9725a02 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 14:14:22 -0500 Subject: [PATCH 25/38] Accounting for older mail clients showing table borders --- .../src/react-email/defaultSchema/blocks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index c277a9623a..31cc03f9c1 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -124,7 +124,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< const headerColsCount = (table.headerCols as number) ?? 0; return ( - +
                            {table.rows.map((row: any, rowIndex: number) => ( From 89afbaf9aeeb1a3128d2fff97c2c0c096f67eecc Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 14:15:14 -0500 Subject: [PATCH 26/38] Correcting type script for older tables --- .../src/react-email/defaultSchema/blocks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 31cc03f9c1..8dcb5ca109 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -124,7 +124,7 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< const headerColsCount = (table.headerCols as number) ?? 0; return ( -
                            +
                            {table.rows.map((row: any, rowIndex: number) => ( From 89ff0f6ea5d77264a96c10b02c87056c3f20ea06 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 14:23:08 -0500 Subject: [PATCH 27/38] Fixing Indentions --- .../src/react-email/reactEmailExporter.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 6e969c794b..30ac4439a5 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -147,7 +147,11 @@ export class ReactEmailExporter< listItems.push(
                          1. {liContent} - {nestedContent.length > 0 && nestedContent} + {nestedContent.length > 0 && ( +
                            + {nestedContent} +
                            + )}
                          2. ); j++; @@ -168,9 +172,13 @@ export class ReactEmailExporter< } i = j; } else { - // Non-list child, render as normal + // Non-list child, render as normal with indentation const childBlocks = await this.transformBlocks([child], nestingLevel); - nestedList.push(...childBlocks); + nestedList.push( +
                            + {childBlocks} +
                            + ); i++; } } @@ -198,7 +206,11 @@ export class ReactEmailExporter< ret.push(
                            {self}
                            - {children.length > 0 && children} + {children.length > 0 && ( +
                            + {children} +
                            + )}
                            ); i++; From 465c5a0b6eba642bcbabf797558aca93e1d63979 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 14:25:46 -0500 Subject: [PATCH 28/38] Fixing Snapshot capturing and updated snapshot --- .../__snapshots__/reactEmailExporter.test.tsx.snap | 2 +- .../src/react-email/reactEmailExporter.test.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap index b15bc4a121..eaad3b704d 100644 --- a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap +++ b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap @@ -1,3 +1,3 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`react email exporter > should export a document (HTML snapshot) 1`] = `Promise {}`; +exports[`react email exporter > should export a document (HTML snapshot) 1`] = `"

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index c93e46ad36..db91917629 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,5 +1,4 @@ import { describe, it, expect } from "vitest"; -import { render } from "@react-email/render"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { BlockNoteSchema } from "@blocknote/core"; @@ -13,8 +12,7 @@ describe("react email exporter", () => { reactEmailDefaultSchemaMappings ); - const reactElement = await exporter.toReactEmailDocument(testDocument as any); - const html = render(reactElement); + const html = await exporter.toReactEmailDocument(testDocument as any); expect(html).toMatchSnapshot(); }); }); \ No newline at end of file From 456841030db14b3fc40eda4599d0580954fb7689 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Thu, 19 Jun 2025 14:30:08 -0500 Subject: [PATCH 29/38] Updating Test --- .../src/react-email/reactEmailExporter.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index db91917629..c93e46ad36 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import { render } from "@react-email/render"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { BlockNoteSchema } from "@blocknote/core"; @@ -12,7 +13,8 @@ describe("react email exporter", () => { reactEmailDefaultSchemaMappings ); - const html = await exporter.toReactEmailDocument(testDocument as any); + const reactElement = await exporter.toReactEmailDocument(testDocument as any); + const html = render(reactElement); expect(html).toMatchSnapshot(); }); }); \ No newline at end of file From 2dda9bd8be2f7ffc68172d72ecaa9dcd5a58bf41 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Mon, 23 Jun 2025 14:19:03 -0500 Subject: [PATCH 30/38] Using mapTableCell --- .../src/react-email/defaultSchema/blocks.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index 8dcb5ca109..a59875e4e4 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -2,6 +2,7 @@ import { DefaultBlockSchema, StyledText } from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; import { CodeBlock, dracula, Heading, Img, Link, PrismLanguage, Text,} from "@react-email/components"; import { pageBreakSchema } from "@blocknote/core"; +import { mapTableCell } from "@blocknote/core/src/util/table.js"; export const reactEmailBlockMappingForDefaultSchema: BlockMapping< DefaultBlockSchema & typeof pageBreakSchema.blockSchema, @@ -129,6 +130,9 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< {table.rows.map((row: any, rowIndex: number) => ( {row.cells.map((cell: any, colIndex: number) => { + // Use mapTableCell to normalize table cell data into a standard interface + // This handles partial cells, provides default values, and ensures consistent structure + const normalizedCell = mapTableCell(cell); const isHeaderRow = rowIndex < headerRowsCount; const isHeaderCol = colIndex < headerColsCount; const isHeader = isHeaderRow || isHeaderCol; @@ -139,12 +143,21 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< style={{ border: '1px solid #ddd', padding: '8px 12px', - background: isHeader ? '#f5f5f5' : '#fff', + background: isHeader + ? '#f5f5f5' + : normalizedCell.props.backgroundColor !== 'default' + ? normalizedCell.props.backgroundColor + : '#fff', fontWeight: isHeader ? 'bold' : 'normal', - textAlign: cell.props?.textAlignment || 'left', + textAlign: normalizedCell.props.textAlignment || 'left', + color: normalizedCell.props.textColor !== 'default' + ? normalizedCell.props.textColor + : 'inherit', }} + {...((normalizedCell.props.colspan || 1) > 1 && { colSpan: normalizedCell.props.colspan || 1 })} + {...((normalizedCell.props.rowspan || 1) > 1 && { rowSpan: normalizedCell.props.rowspan || 1 })} > - {t.transformInlineContent(cell.content)} + {t.transformInlineContent(normalizedCell.content)} ); })} From ae7027e9fb748db4591e0ce08e6845a761ea4ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Marbutt Date: Mon, 23 Jun 2025 14:23:50 -0500 Subject: [PATCH 31/38] Updating a few commented items --- .../src/react-email/reactEmailExporter.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 30ac4439a5..2ef945524c 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -23,7 +23,7 @@ import { import { render as renderEmail } from "@react-email/render"; import React from "react"; - import { CSSProperties } from "react"; +import { CSSProperties } from "react"; export class ReactEmailExporter< B extends BlockSchema, @@ -76,11 +76,8 @@ export class ReactEmailExporter< const listType = blocks[startIndex].type; const listItems: React.ReactElement[] = []; let j = startIndex; - let itemIndex = 1; - while ( - j < blocks.length && - blocks[j].type === listType - ) { + + for (let itemIndex = 1; j < blocks.length && blocks[j].type === listType; j++, itemIndex++) { const block = blocks[j]; const liContent = await this.mapBlock(block as any, nestingLevel, itemIndex) as any; let nestedList: React.ReactElement[] = []; @@ -93,8 +90,6 @@ export class ReactEmailExporter< {nestedList.length > 0 && nestedList} ); - j++; - itemIndex++; } let element: React.ReactElement; if (listType === "bulletListItem") { @@ -127,8 +122,8 @@ export class ReactEmailExporter< const listType = child.type; const listItems: React.ReactElement[] = []; let j = i; - let itemIndex = 1; - while (j < children.length && children[j].type === listType) { + + for (let itemIndex = 1; j < children.length && children[j].type === listType; j++, itemIndex++) { const listItem = children[j]; const liContent = await this.mapBlock(listItem as any, nestingLevel, itemIndex) as any; const style = this.blocknoteDefaultPropsToReactEmailStyle(listItem.props as any); @@ -154,8 +149,6 @@ export class ReactEmailExporter< )} ); - j++; - itemIndex++; } if (listType === "bulletListItem") { nestedList.push( From ed57add0a5ffae759182674e60be99d17798e340 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 11:37:25 +0200 Subject: [PATCH 32/38] chore: reformat files --- packages/xl-react-email-exporter/package.json | 169 +++--- packages/xl-react-email-exporter/src/index.ts | 2 +- .../reactEmailExporter.test.tsx.snap | 2 +- .../src/react-email/defaultSchema/blocks.tsx | 519 ++++++++++------- .../src/react-email/defaultSchema/index.ts | 2 +- .../defaultSchema/inlinecontent.tsx | 50 +- .../src/react-email/defaultSchema/styles.tsx | 7 +- .../src/react-email/index.ts | 2 +- .../react-email/reactEmailExporter.test.tsx | 13 +- .../src/react-email/reactEmailExporter.tsx | 526 ++++++++++-------- pnpm-lock.yaml | 20 +- 11 files changed, 742 insertions(+), 570 deletions(-) diff --git a/packages/xl-react-email-exporter/package.json b/packages/xl-react-email-exporter/package.json index 0e387e5871..35b4ca18fc 100644 --- a/packages/xl-react-email-exporter/package.json +++ b/packages/xl-react-email-exporter/package.json @@ -1,86 +1,87 @@ { - "name": "@blocknote/xl-react-email-exporter", - "homepage": "https://github.com/TypeCellOS/BlockNote", - "private": true, - "license": "AGPL-3.0", - "version": "0.18.1", - "files": [ - "dist", - "types", - "src" - ], - "keywords": [ - "react", - "javascript", - "editor", - "typescript", - "prosemirror", - "wysiwyg", - "rich-text-editor", - "notion", - "yjs", - "block-based", - "tiptap" - ], - "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.", - "type": "module", - "source": "src/index.ts", - "types": "./types/src/index.d.ts", - "main": "./dist/blocknote-xl-react-email-exporter.umd.cjs", - "module": "./dist/blocknote-xl-react-email-exporter.js", - "exports": { - ".": { - "types": "./types/src/index.d.ts", - "import": "./dist/blocknote-xl-react-email-exporter.js", - "require": "./dist/blocknote-xl-react-email-exporter.umd.cjs" - }, - "./style.css": { - "import": "./dist/style.css", - "require": "./dist/style.css" - } + "name": "@blocknote/xl-react-email-exporter", + "homepage": "https://github.com/TypeCellOS/BlockNote", + "private": false, + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/TypeCellOS/BlockNote.git", + "directory": "packages/xl-react-email-exporter" + }, + "license": "AGPL-3.0 OR PROPRIETARY", + "version": "0.31.3", + "files": [ + "dist", + "types", + "src" + ], + "keywords": [ + "react", + "javascript", + "editor", + "typescript", + "prosemirror", + "wysiwyg", + "rich-text-editor", + "notion", + "yjs", + "block-based", + "tiptap" + ], + "description": "A \"Notion-style\" block-based extensible text editor built on top of Prosemirror and Tiptap.", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-xl-react-email-exporter.umd.cjs", + "module": "./dist/blocknote-xl-react-email-exporter.js", + "exports": { + ".": { + "types": "./types/src/index.d.ts", + "import": "./dist/blocknote-xl-react-email-exporter.js", + "require": "./dist/blocknote-xl-react-email-exporter.umd.cjs" }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint src --max-warnings 0", - "test": "vitest --run", - "test-watch": "vitest watch", - "email": "email dev" - }, - "dependencies": { - "@blocknote/core": "0.31.3", - "@blocknote/react": "0.31.3", - "buffer": "^6.0.3", - "react": "^18", - "react-dom": "^18", - "react-email": "^4.0.16", - "@react-email/components": "^0.1.0", - "@react-email/render": "^1.1.2" - }, - "devDependencies": { - "@types/jsdom": "^21.1.7", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", - "eslint": "^8.10.0", - "prettier": "^2.7.1", - "rollup-plugin-webpack-stats": "^0.2.2", - "typescript": "^5.0.4", - "vite": "^5.3.4", - "vite-plugin-eslint": "^1.8.1", - "vitest": "^2.0.3" - }, - "peerDependencies": { - "react": "^18", - "react-dom": "^18" - }, - "eslintConfig": { - "extends": [ - "../../.eslintrc.js" - ] - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" -} \ No newline at end of file + "./style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --max-warnings 0", + "test": "vitest --run", + "test-watch": "vitest watch", + "email": "email dev" + }, + "dependencies": { + "@blocknote/core": "0.31.3", + "@blocknote/react": "0.31.3", + "buffer": "^6.0.3", + "react": "^18", + "react-dom": "^18", + "react-email": "^4.0.16", + "@react-email/components": "^0.1.0", + "@react-email/render": "^1.1.2" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "eslint": "^8.10.0", + "rollup-plugin-webpack-stats": "^0.2.2", + "typescript": "^5.0.4", + "vite": "^5.3.4", + "vite-plugin-eslint": "^1.8.1", + "vitest": "^2.0.3" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || >= 19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.js" + ] + }, + "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" +} diff --git a/packages/xl-react-email-exporter/src/index.ts b/packages/xl-react-email-exporter/src/index.ts index b1fd5b0856..0bb826bc42 100644 --- a/packages/xl-react-email-exporter/src/index.ts +++ b/packages/xl-react-email-exporter/src/index.ts @@ -1 +1 @@ -export * from "./react-email/index.js"; \ No newline at end of file +export * from "./react-email/index.js"; diff --git a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap index eaad3b704d..bd8478d3ad 100644 --- a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap +++ b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap @@ -1,3 +1,3 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`react email exporter > should export a document (HTML snapshot) 1`] = `"

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; +exports[`react email exporter > should export a document (HTML snapshot) > __snapshots__/reactEmailExporter 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><html dir="ltr" lang="en"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="font-family:&#x27;SF Pro Display&#x27;, -apple-system, BlinkMacSystemFont, &#x27;Segoe UI&#x27;, &#x27;Roboto&#x27;, &#x27;Helvetica Neue&#x27;, Arial, sans-serif;font-size:16px;line-height:1.5;color:#333"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-style:italic">Welcome to this </span><span style="font-style:italic;font-weight:bold">demo 🙌!</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World nested</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World double nested</span></p></td></tr></tbody></table></div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left;background-color:#fbe4e4"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">This paragraph has a background color</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Paragraph</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><h1><span>Heading</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><h1><span>Heading right</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:justify"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><hr style="border:none;border-top:2px dashed #ccc;margin:24px 0"/></td></tr></tbody></table><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li><li style="text-align:right;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 2</span></p><div style="margin-top:8px"><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 2</span></p></li><li style="text-align:right;background-color:#fbe4e4;color:#0b6e99;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky right</span></p></li><li style="text-align:center;background-color:#fbe4e4;color:#0b6e99;align-items:center"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky center</span></p></li></ol></div></li></ol></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item</span></p></ol><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><svg width="18" height="18" viewBox="0 0 18 18" style="display:inline;vertical-align:middle;margin-right:8px"><rect x="2" y="2" width="14" height="14" rx="3" fill="#fff" stroke="#888" stroke-width="2"></rect></svg><span><span>Check List Item</span></span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#888" style="display:inline;vertical-align:middle"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg><span style="vertical-align:middle">Open file</span></a></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><img alt="From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><img alt="" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none" width="200"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#1976D2" style="display:inline;vertical-align:middle"><path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM8 5V19H16V5H8ZM4 5V7H6V5H4ZM18 5V7H20V5H18ZM4 9V11H6V9H4ZM18 9V11H20V9H18ZM4 13V15H6V13H4ZM18 13V15H20V13H18ZM4 17V19H6V17H4ZM18 17V19H20V17H18Z"></path></svg><span style="vertical-align:middle">Open video file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">Open audio file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">audio.mp3</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">Audio file caption</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">Inline Content:</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold;font-style:italic;color:red;background-color:blue">Styled Text</span><span> </span><a href="https://www.blocknotejs.org" style="color:#067df7;text-decoration-line:none" target="_blank"><span>Link</span></a></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 1</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 2</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 3</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 4</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span style="font-weight:bold">Table Cell Bold 5</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 6</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 7</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 8</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 9</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><pre style="color:#f8f8f2;background:#282a36;text-shadow:0 1px rgba(0, 0, 0, 0.3);font-family:Consolas, Monaco, &#x27;Andale Mono&#x27;, &#x27;Ubuntu Mono&#x27;, monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;border-radius:0.3em;width:100%"><code><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#8be9fd">const</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">helloWorld</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace">message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=&gt;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">{</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">console</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">.</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">log</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#50fa7b">&quot;Hello World&quot;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">,</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">}</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p></code></pre></td></tr></tbody></table></td></tr></tbody></table></body></html><!--/$-->"`; diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx index a59875e4e4..1585fa4a22 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -1,8 +1,19 @@ -import { DefaultBlockSchema, StyledText } from "@blocknote/core"; +import { + DefaultBlockSchema, + mapTableCell, + pageBreakSchema, + StyledText, +} from "@blocknote/core"; import { BlockMapping } from "@blocknote/core/src/exporter/mapping.js"; -import { CodeBlock, dracula, Heading, Img, Link, PrismLanguage, Text,} from "@react-email/components"; -import { pageBreakSchema } from "@blocknote/core"; -import { mapTableCell } from "@blocknote/core/src/util/table.js"; +import { + CodeBlock, + dracula, + Heading, + Img, + Link, + PrismLanguage, + Text, +} from "@react-email/components"; export const reactEmailBlockMappingForDefaultSchema: BlockMapping< DefaultBlockSchema & typeof pageBreakSchema.blockSchema, @@ -11,199 +22,323 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< React.ReactElement, React.ReactElement | React.ReactElement > = { - paragraph: (block, t) => { - return {t.transformInlineContent(block.content)}; - }, - bulletListItem: (block, t) => { - // Return only the
                          3. for grouping in the exporter - return {t.transformInlineContent(block.content)}; - }, - numberedListItem: (block, t, _nestingLevel) => { - // Return only the
                          4. for grouping in the exporter - return {t.transformInlineContent(block.content)}; - }, - checkListItem: (block, t) => { - // Render a checkbox using inline SVG for better appearance in email - // block.props.checked should be true/false - const checked = block.props?.checked; - const checkboxSvg = checked ? ( - - - - - ) : ( - - - - ); - return ( - - {checkboxSvg} - {t.transformInlineContent(block.content)} - - ); - }, - heading: (block, t) => { - return ( - - {t.transformInlineContent(block.content)} - - ); - }, - - codeBlock: (block) => { - const textContent = (block.content as StyledText[])[0]?.text || ""; - - return { + return {t.transformInlineContent(block.content)}; + }, + bulletListItem: (block, t) => { + // Return only the
                          5. for grouping in the exporter + return {t.transformInlineContent(block.content)}; + }, + numberedListItem: (block, t, _nestingLevel) => { + // Return only the
                          6. for grouping in the exporter + return {t.transformInlineContent(block.content)}; + }, + checkListItem: (block, t) => { + // Render a checkbox using inline SVG for better appearance in email + // block.props.checked should be true/false + const checked = block.props?.checked; + const checkboxSvg = checked ? ( + + + + + ) : ( + + - - }, - audio: (block) => { - // Audio icon SVG - const icon = ( - - - - ); - const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; - return ( -
                            - - -
                            - ); - }, - video: (block) => { - // Video icon SVG - const icon = ( - - - - ); - const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; - return ( -
                            - - -
                            - ); - }, - file: (block) => { - // File icon SVG - const icon = ( - - - - ); - const previewWidth = 'previewWidth' in block.props ? (block.props as any).previewWidth : undefined; - return ( -
                            - - -
                            - ); - }, - image: (block) => { - return ( - {block.props.caption} - ); - }, - table: (block, t) => { - // Render table using standard HTML table elements for email compatibility - const table = block.content; - if (!table || typeof table !== 'object' || !Array.isArray(table.rows)) { - return Table data not available; - } - const headerRowsCount = (table.headerRows as number) ?? 0; - const headerColsCount = (table.headerCols as number) ?? 0; + + ); + return ( + + {checkboxSvg} + {t.transformInlineContent(block.content)} + + ); + }, + heading: (block, t) => { + return ( + + {t.transformInlineContent(block.content)} + + ); + }, - return ( - - - {table.rows.map((row: any, rowIndex: number) => ( - - {row.cells.map((cell: any, colIndex: number) => { - // Use mapTableCell to normalize table cell data into a standard interface - // This handles partial cells, provides default values, and ensures consistent structure - const normalizedCell = mapTableCell(cell); - const isHeaderRow = rowIndex < headerRowsCount; - const isHeaderCol = colIndex < headerColsCount; - const isHeader = isHeaderRow || isHeaderCol; - const CellTag = isHeader ? 'th' : 'td'; - return ( - 1 && { colSpan: normalizedCell.props.colspan || 1 })} - {...((normalizedCell.props.rowspan || 1) > 1 && { rowSpan: normalizedCell.props.rowspan || 1 })} - > - {t.transformInlineContent(normalizedCell.content)} - - ); - })} - - ))} - -
                            - ); - }, - quote: (block, t) => { - // Render block quote with a left border and subtle background for email compatibility - return ( - - {t.transformInlineContent(block.content)} - - ); - }, - pageBreak: () => { - // In email, a page break can be represented as a horizontal rule - return
                            ; - }, -}; + codeBlock: (block) => { + const textContent = (block.content as StyledText[])[0]?.text || ""; -// Helper for file-like blocks (audio, video, file) -function FileLink({ url, name, defaultText, icon }: { url?: string, name?: string, defaultText: string, icon: React.ReactElement }) { return ( - - {icon} - {name || defaultText} - + ); -} + }, + audio: (block) => { + // Audio icon SVG + const icon = ( + + + + ); + const previewWidth = + "previewWidth" in block.props + ? (block.props as any).previewWidth + : undefined; + return ( +
                            + + +
                            + ); + }, + video: (block) => { + // Video icon SVG + const icon = ( + + + + ); + const previewWidth = + "previewWidth" in block.props + ? (block.props as any).previewWidth + : undefined; + return ( +
                            + + +
                            + ); + }, + file: (block) => { + // File icon SVG + const icon = ( + + + + ); + const previewWidth = + "previewWidth" in block.props + ? (block.props as any).previewWidth + : undefined; + return ( +
                            + + +
                            + ); + }, + image: (block) => { + return ( + {block.props.caption} + ); + }, + table: (block, t) => { + // Render table using standard HTML table elements for email compatibility + const table = block.content; + if (!table || typeof table !== "object" || !Array.isArray(table.rows)) { + return Table data not available; + } + const headerRowsCount = (table.headerRows as number) ?? 0; + const headerColsCount = (table.headerCols as number) ?? 0; -function Caption({ caption, width }: { caption?: string, width?: number }) { - if (!caption) return null; return ( - {caption} + + + {table.rows.map((row: any, rowIndex: number) => ( + + {row.cells.map((cell: any, colIndex: number) => { + // Use mapTableCell to normalize table cell data into a standard interface + // This handles partial cells, provides default values, and ensures consistent structure + const normalizedCell = mapTableCell(cell); + const isHeaderRow = rowIndex < headerRowsCount; + const isHeaderCol = colIndex < headerColsCount; + const isHeader = isHeaderRow || isHeaderCol; + const CellTag = isHeader ? "th" : "td"; + return ( + 1 && { + colSpan: normalizedCell.props.colspan || 1, + })} + {...((normalizedCell.props.rowspan || 1) > 1 && { + rowSpan: normalizedCell.props.rowspan || 1, + })} + > + {t.transformInlineContent(normalizedCell.content)} + + ); + })} + + ))} + +
                            + ); + }, + quote: (block, t) => { + // Render block quote with a left border and subtle background for email compatibility + return ( + + {t.transformInlineContent(block.content)} + ); -} \ No newline at end of file + }, + pageBreak: () => { + // In email, a page break can be represented as a horizontal rule + return ( +
                            + ); + }, +}; + +// Helper for file-like blocks (audio, video, file) +function FileLink({ + url, + name, + defaultText, + icon, +}: { + url?: string; + name?: string; + defaultText: string; + icon: React.ReactElement; +}) { + return ( + + {icon} + {name || defaultText} + + ); +} + +function Caption({ caption, width }: { caption?: string; width?: number }) { + if (!caption) return null; + return ( + + {caption} + + ); +} diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts b/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts index 320c91202e..0e7e4bf0f1 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts @@ -6,4 +6,4 @@ export const reactEmailDefaultSchemaMappings = { blockMapping: reactEmailBlockMappingForDefaultSchema, inlineContentMapping: reactEmailInlineContentMappingForDefaultSchema, styleMapping: reactEmailStyleMappingForDefaultSchema, -}; \ No newline at end of file +}; diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx index 9d02c32e87..bbb15ae9dd 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx @@ -1,26 +1,26 @@ import { - DefaultInlineContentSchema, - DefaultStyleSchema, - } from "@blocknote/core"; - import { InlineContentMapping } from "@blocknote/core/src/exporter/mapping.js"; - import { Link } from "@react-email/components"; - - export const reactEmailInlineContentMappingForDefaultSchema: InlineContentMapping< - DefaultInlineContentSchema, - DefaultStyleSchema, - React.ReactElement | React.ReactElement, - React.ReactElement - > = { - link: (ic, t) => { - return ( - - {...ic.content.map((content) => { - return t.transformStyledText(content); - })} - - ); - }, - text: (ic, t) => { - return t.transformStyledText(ic); - }, - }; \ No newline at end of file + DefaultInlineContentSchema, + DefaultStyleSchema, +} from "@blocknote/core"; +import { InlineContentMapping } from "@blocknote/core/src/exporter/mapping.js"; +import { Link } from "@react-email/components"; + +export const reactEmailInlineContentMappingForDefaultSchema: InlineContentMapping< + DefaultInlineContentSchema, + DefaultStyleSchema, + React.ReactElement | React.ReactElement, + React.ReactElement +> = { + link: (ic, t) => { + return ( + + {...ic.content.map((content) => { + return t.transformStyledText(content); + })} + + ); + }, + text: (ic, t) => { + return t.transformStyledText(ic); + }, +}; diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx index b7a5ea110d..9b86345139 100644 --- a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx +++ b/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx @@ -1,5 +1,4 @@ -import { DefaultStyleSchema } from "@blocknote/core"; -import { StyleMapping } from "@blocknote/core/src/exporter/mapping.js"; +import { DefaultStyleSchema, StyleMapping } from "@blocknote/core"; import { CSSProperties } from "react"; export const reactEmailStyleMappingForDefaultSchema: StyleMapping< @@ -50,7 +49,7 @@ export const reactEmailStyleMappingForDefaultSchema: StyleMapping< return { color: val, }; - }, + }, code: (val) => { if (!val) { return {}; @@ -59,4 +58,4 @@ export const reactEmailStyleMappingForDefaultSchema: StyleMapping< fontFamily: "Courier", }; }, -}; \ No newline at end of file +}; diff --git a/packages/xl-react-email-exporter/src/react-email/index.ts b/packages/xl-react-email-exporter/src/react-email/index.ts index 83b1c99f52..6d652034be 100644 --- a/packages/xl-react-email-exporter/src/react-email/index.ts +++ b/packages/xl-react-email-exporter/src/react-email/index.ts @@ -1,2 +1,2 @@ export * from "./defaultSchema/index"; -export * from "./reactEmailExporter"; \ No newline at end of file +export * from "./reactEmailExporter"; diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index c93e46ad36..058af57cfc 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -5,16 +5,17 @@ import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { BlockNoteSchema } from "@blocknote/core"; import { testDocument } from "@shared/testDocument.js"; - describe("react email exporter", () => { it("should export a document (HTML snapshot)", async () => { const exporter = new ReactEmailExporter( BlockNoteSchema.create(), - reactEmailDefaultSchemaMappings + reactEmailDefaultSchemaMappings, ); - const reactElement = await exporter.toReactEmailDocument(testDocument as any); - const html = render(reactElement); - expect(html).toMatchSnapshot(); + const reactElement = await exporter.toReactEmailDocument( + testDocument as any, + ); + const html = await render(reactElement); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporter"); }); -}); \ No newline at end of file +}); diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx index 2ef945524c..a1427a5ff9 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx @@ -1,265 +1,311 @@ import { - Block, - BlockNoteSchema, - BlockSchema, - COLORS_DEFAULT, - DefaultProps, - InlineContentSchema, - StyleSchema, - StyledText, - } from "@blocknote/core"; - - import { Exporter, ExporterOptions } from "@blocknote/core"; - - import { - Body, - Container, - Head, - Html, - Link, - Preview, - Section, - Tailwind } from "@react-email/components"; - import { render as renderEmail } from "@react-email/render"; + Block, + BlockNoteSchema, + BlockSchema, + COLORS_DEFAULT, + DefaultProps, + Exporter, + ExporterOptions, + InlineContentSchema, + StyleSchema, + StyledText, +} from "@blocknote/core"; + +import { + Body, + Container, + Head, + Html, + Link, + Preview, + Section, + Tailwind, +} from "@react-email/components"; +import { render as renderEmail } from "@react-email/render"; + +import React, { CSSProperties } from "react"; -import React from "react"; -import { CSSProperties } from "react"; - export class ReactEmailExporter< - B extends BlockSchema, - S extends StyleSchema, - I extends InlineContentSchema - > extends Exporter< - B, - I, - S, - React.ReactElement, - React.ReactElement | React.ReactElement, - CSSProperties, - React.ReactElement - > { - public constructor( - public readonly schema: BlockNoteSchema, - mappings: Exporter< - NoInfer, - NoInfer, - NoInfer, - React.ReactElement, - React.ReactElement | React.ReactElement, - CSSProperties, - React.ReactElement - >["mappings"], - options?: Partial + B extends BlockSchema, + S extends StyleSchema, + I extends InlineContentSchema, +> extends Exporter< + B, + I, + S, + React.ReactElement, + React.ReactElement | React.ReactElement, + CSSProperties, + React.ReactElement +> { + public constructor( + public readonly schema: BlockNoteSchema, + mappings: Exporter< + NoInfer, + NoInfer, + NoInfer, + React.ReactElement, + React.ReactElement | React.ReactElement, + CSSProperties, + React.ReactElement + >["mappings"], + options?: Partial, + ) { + const defaults = { + colors: COLORS_DEFAULT, + } satisfies Partial; + + const newOptions = { + ...defaults, + ...options, + }; + super(schema, mappings, newOptions); + } + + public transformStyledText(styledText: StyledText) { + const stylesArray = this.mapStyles(styledText.styles); + const styles = Object.assign({}, ...stylesArray); + return {styledText.text}; + } + + private async renderGroupedListBlocks( + blocks: Block[], + startIndex: number, + nestingLevel: number, + ): Promise<{ element: React.ReactElement; nextIndex: number }> { + const listType = blocks[startIndex].type; + const listItems: React.ReactElement[] = []; + let j = startIndex; + + for ( + let itemIndex = 1; + j < blocks.length && blocks[j].type === listType; + j++, itemIndex++ ) { - const defaults = { - colors: COLORS_DEFAULT, - } satisfies Partial; - - const newOptions = { - ...defaults, - ...options, - }; - super(schema, mappings, newOptions); - } - - public transformStyledText(styledText: StyledText) { - const stylesArray = this.mapStyles(styledText.styles); - const styles = Object.assign({}, ...stylesArray); - return {styledText.text}; - } - - private async renderGroupedListBlocks( - blocks: Block[], - startIndex: number, - nestingLevel: number - ): Promise<{ element: React.ReactElement, nextIndex: number }> { - const listType = blocks[startIndex].type; - const listItems: React.ReactElement[] = []; - let j = startIndex; - - for (let itemIndex = 1; j < blocks.length && blocks[j].type === listType; j++, itemIndex++) { - const block = blocks[j]; - const liContent = await this.mapBlock(block as any, nestingLevel, itemIndex) as any; - let nestedList: React.ReactElement[] = []; - if (block.children && block.children.length > 0) { - nestedList = await this.renderNestedLists(block.children, nestingLevel + 1, block.id); - } - listItems.push( - - {liContent} - {nestedList.length > 0 && nestedList} - + const block = blocks[j]; + const liContent = (await this.mapBlock( + block as any, + nestingLevel, + itemIndex, + )) as any; + let nestedList: React.ReactElement[] = []; + if (block.children && block.children.length > 0) { + nestedList = await this.renderNestedLists( + block.children, + nestingLevel + 1, + block.id, ); } - let element: React.ReactElement; - if (listType === "bulletListItem") { - element = ( -
                              - {listItems} -
                            - ); - } else { - element = ( -
                              - {listItems} -
                            - ); - } - return { element, nextIndex: j }; + listItems.push( + + {liContent} + {nestedList.length > 0 && nestedList} + , + ); } + let element: React.ReactElement; + if (listType === "bulletListItem") { + element = ( +
                              + {listItems} +
                            + ); + } else { + element = ( +
                              + {listItems} +
                            + ); + } + return { element, nextIndex: j }; + } + + private async renderNestedLists( + children: Block[], + nestingLevel: number, + parentId: string, + ): Promise[]> { + const nestedList: React.ReactElement[] = []; + let i = 0; + while (i < children.length) { + const child = children[i]; + if ( + child.type === "bulletListItem" || + child.type === "numberedListItem" + ) { + // Group consecutive list items of the same type + const listType = child.type; + const listItems: React.ReactElement[] = []; + let j = i; - private async renderNestedLists( - children: Block[], - nestingLevel: number, - parentId: string - ): Promise[]> { - const nestedList: React.ReactElement[] = []; - let i = 0; - while (i < children.length) { - const child = children[i]; - if (child.type === "bulletListItem" || child.type === "numberedListItem") { - // Group consecutive list items of the same type - const listType = child.type; - const listItems: React.ReactElement[] = []; - let j = i; - - for (let itemIndex = 1; j < children.length && children[j].type === listType; j++, itemIndex++) { - const listItem = children[j]; - const liContent = await this.mapBlock(listItem as any, nestingLevel, itemIndex) as any; - const style = this.blocknoteDefaultPropsToReactEmailStyle(listItem.props as any); - let nestedContent: React.ReactElement[] = []; - if (listItem.children && listItem.children.length > 0) { - // If children are list items, render as nested list; otherwise, as normal blocks - if ( - listItem.children[0] && - (listItem.children[0].type === "bulletListItem" || listItem.children[0].type === "numberedListItem") - ) { - nestedContent = await this.renderNestedLists(listItem.children, nestingLevel + 1, listItem.id); - } else { - nestedContent = await this.transformBlocks(listItem.children, nestingLevel + 1); - } + for ( + let itemIndex = 1; + j < children.length && children[j].type === listType; + j++, itemIndex++ + ) { + const listItem = children[j]; + const liContent = (await this.mapBlock( + listItem as any, + nestingLevel, + itemIndex, + )) as any; + const style = this.blocknoteDefaultPropsToReactEmailStyle( + listItem.props as any, + ); + let nestedContent: React.ReactElement[] = []; + if (listItem.children && listItem.children.length > 0) { + // If children are list items, render as nested list; otherwise, as normal blocks + if ( + listItem.children[0] && + (listItem.children[0].type === "bulletListItem" || + listItem.children[0].type === "numberedListItem") + ) { + nestedContent = await this.renderNestedLists( + listItem.children, + nestingLevel + 1, + listItem.id, + ); + } else { + nestedContent = await this.transformBlocks( + listItem.children, + nestingLevel + 1, + ); } - listItems.push( -
                          7. - {liContent} - {nestedContent.length > 0 && ( -
                            - {nestedContent} -
                            - )} -
                          8. - ); - } - if (listType === "bulletListItem") { - nestedList.push( -
                              - {listItems} -
                            - ); - } else { - nestedList.push( -
                              - {listItems} -
                            - ); } - i = j; + listItems.push( +
                          9. + {liContent} + {nestedContent.length > 0 && ( +
                            {nestedContent}
                            + )} +
                          10. , + ); + } + if (listType === "bulletListItem") { + nestedList.push( +
                              + {listItems} +
                            , + ); } else { - // Non-list child, render as normal with indentation - const childBlocks = await this.transformBlocks([child], nestingLevel); nestedList.push( -
                            - {childBlocks} -
                            +
                              + {listItems} +
                            , ); - i++; } + i = j; + } else { + // Non-list child, render as normal with indentation + const childBlocks = await this.transformBlocks([child], nestingLevel); + nestedList.push( +
                            + {childBlocks} +
                            , + ); + i++; } - return nestedList; } + return nestedList; + } - public async transformBlocks( - blocks: Block[], - nestingLevel = 0 - ): Promise[]> { - const ret: React.ReactElement[] = []; - let i = 0; - while (i < blocks.length) { - const b = blocks[i]; - if (b.type === "bulletListItem" || b.type === "numberedListItem") { - const { element, nextIndex } = await this.renderGroupedListBlocks(blocks, i, nestingLevel); - ret.push(element); - i = nextIndex; - continue; - } - // Non-list blocks - const children = await this.transformBlocks(b.children, nestingLevel + 1); - const self = await this.mapBlock(b as any, nestingLevel, 0) as any; - const style = this.blocknoteDefaultPropsToReactEmailStyle(b.props as any); - ret.push( - -
                            {self}
                            - {children.length > 0 && ( -
                            - {children} -
                            - )} -
                            + public async transformBlocks( + blocks: Block[], + nestingLevel = 0, + ): Promise[]> { + const ret: React.ReactElement[] = []; + let i = 0; + while (i < blocks.length) { + const b = blocks[i]; + if (b.type === "bulletListItem" || b.type === "numberedListItem") { + const { element, nextIndex } = await this.renderGroupedListBlocks( + blocks, + i, + nestingLevel, ); - i++; + ret.push(element); + i = nextIndex; + continue; } - return ret; + // Non-list blocks + const children = await this.transformBlocks(b.children, nestingLevel + 1); + const self = (await this.mapBlock(b as any, nestingLevel, 0)) as any; + const style = this.blocknoteDefaultPropsToReactEmailStyle(b.props as any); + ret.push( + +
                            {self}
                            + {children.length > 0 && ( +
                            {children}
                            + )} +
                            , + ); + i++; } + return ret; + } - public async toReactEmailDocument( - blocks: Block[], - options?: { - preview?: string | string[]; - } - ) { - const transformedBlocks = await this.transformBlocks(blocks); - return renderEmail( - - - [], + options?: { + preview?: string | string[]; + }, + ) { + const transformedBlocks = await this.transformBlocks(blocks); + return renderEmail( + + + - {options?.preview && {options.preview}} - - {transformedBlocks} - - - - ); - } + }} + > + {options?.preview && {options.preview}} + + {transformedBlocks} + + + , + ); + } - - protected blocknoteDefaultPropsToReactEmailStyle( - props: Partial, - ): any { - return { - textAlign: props.textAlignment, - backgroundColor: - props.backgroundColor === "default" || !props.backgroundColor - ? undefined - : this.options.colors[ - props.backgroundColor as keyof typeof this.options.colors - ].background, - color: - props.textColor === "default" || !props.textColor - ? undefined - : this.options.colors[ - props.textColor as keyof typeof this.options.colors - ].text, - alignItems: - props.textAlignment === "right" - ? "flex-end" - : props.textAlignment === "center" - ? "center" - : undefined, - }; - } - } \ No newline at end of file + protected blocknoteDefaultPropsToReactEmailStyle( + props: Partial, + ): any { + return { + textAlign: props.textAlignment, + backgroundColor: + props.backgroundColor === "default" || !props.backgroundColor + ? undefined + : this.options.colors[ + props.backgroundColor as keyof typeof this.options.colors + ].background, + color: + props.textColor === "default" || !props.textColor + ? undefined + : this.options.colors[ + props.textColor as keyof typeof this.options.colors + ].text, + alignItems: + props.textAlignment === "right" + ? "flex-end" + : props.textAlignment === "center" + ? "center" + : undefined, + }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85ff85d78..866fb1d3dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4221,9 +4221,6 @@ importers: eslint: specifier: ^8.10.0 version: 8.57.1 - prettier: - specifier: ^2.7.1 - version: 2.8.8 rollup-plugin-webpack-stats: specifier: ^0.2.2 version: 0.2.6(rollup@4.37.0) @@ -13164,11 +13161,6 @@ packages: prettier-plugin-svelte: optional: true - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -22122,7 +22114,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) @@ -22169,7 +22161,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -22184,14 +22176,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -22214,7 +22206,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -25501,8 +25493,6 @@ snapshots: dependencies: prettier: 3.5.3 - prettier@2.8.8: {} - prettier@3.5.3: {} pretty-format@27.5.1: From 18784aaee8d0bdc99bd09c01175c66801f4dace7 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 11:50:19 +0200 Subject: [PATCH 33/38] test: add more tests --- .../reactEmailExporter.test.tsx.snap | 32 + .../react-email/reactEmailExporter.test.tsx | 765 +++++++++++++++++- vitest.workspace.ts | 1 + 3 files changed, 797 insertions(+), 1 deletion(-) diff --git a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap index bd8478d3ad..30c707c8d3 100644 --- a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap +++ b/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap @@ -1,3 +1,35 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`react email exporter > should export a document (HTML snapshot) > __snapshots__/reactEmailExporter 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><html dir="ltr" lang="en"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="font-family:&#x27;SF Pro Display&#x27;, -apple-system, BlinkMacSystemFont, &#x27;Segoe UI&#x27;, &#x27;Roboto&#x27;, &#x27;Helvetica Neue&#x27;, Arial, sans-serif;font-size:16px;line-height:1.5;color:#333"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-style:italic">Welcome to this </span><span style="font-style:italic;font-weight:bold">demo 🙌!</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World nested</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World double nested</span></p></td></tr></tbody></table></div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left;background-color:#fbe4e4"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">This paragraph has a background color</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Paragraph</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><h1><span>Heading</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><h1><span>Heading right</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:justify"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><hr style="border:none;border-top:2px dashed #ccc;margin:24px 0"/></td></tr></tbody></table><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li><li style="text-align:right;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 2</span></p><div style="margin-top:8px"><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 2</span></p></li><li style="text-align:right;background-color:#fbe4e4;color:#0b6e99;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky right</span></p></li><li style="text-align:center;background-color:#fbe4e4;color:#0b6e99;align-items:center"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky center</span></p></li></ol></div></li></ol></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item</span></p></ol><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><svg width="18" height="18" viewBox="0 0 18 18" style="display:inline;vertical-align:middle;margin-right:8px"><rect x="2" y="2" width="14" height="14" rx="3" fill="#fff" stroke="#888" stroke-width="2"></rect></svg><span><span>Check List Item</span></span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#888" style="display:inline;vertical-align:middle"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg><span style="vertical-align:middle">Open file</span></a></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><img alt="From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><img alt="" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none" width="200"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#1976D2" style="display:inline;vertical-align:middle"><path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM8 5V19H16V5H8ZM4 5V7H6V5H4ZM18 5V7H20V5H18ZM4 9V11H6V9H4ZM18 9V11H20V9H18ZM4 13V15H6V13H4ZM18 13V15H20V13H18ZM4 17V19H6V17H4ZM18 17V19H20V17H18Z"></path></svg><span style="vertical-align:middle">Open video file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">Open audio file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">audio.mp3</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">Audio file caption</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">Inline Content:</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold;font-style:italic;color:red;background-color:blue">Styled Text</span><span> </span><a href="https://www.blocknotejs.org" style="color:#067df7;text-decoration-line:none" target="_blank"><span>Link</span></a></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 1</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 2</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 3</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 4</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span style="font-weight:bold">Table Cell Bold 5</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 6</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 7</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 8</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 9</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><pre style="color:#f8f8f2;background:#282a36;text-shadow:0 1px rgba(0, 0, 0, 0.3);font-family:Consolas, Monaco, &#x27;Andale Mono&#x27;, &#x27;Ubuntu Mono&#x27;, monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;border-radius:0.3em;width:100%"><code><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#8be9fd">const</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">helloWorld</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace">message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=&gt;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">{</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">console</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">.</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">log</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#50fa7b">&quot;Hello World&quot;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">,</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">}</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p></code></pre></td></tr></tbody></table></td></tr></tbody></table></body></html><!--/$-->"`; + +exports[`react email exporter > should export a document with multiple preview lines > __snapshots__/reactEmailExporterWithMultiplePreview 1`] = `"
                            First preview lineSecond preview line
                             ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; + +exports[`react email exporter > should export a document with preview > __snapshots__/reactEmailExporterWithPreview 1`] = `"
                            This is a preview of the email content
                             ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; + +exports[`react email exporter > should handle document with background colors > __snapshots__/reactEmailExporterBackgroundColor 1`] = `"

                            Text with background color

                            "`; + +exports[`react email exporter > should handle document with check list items > __snapshots__/reactEmailExporterCheckList 1`] = `"

                            Checked item

                            Unchecked item

                            "`; + +exports[`react email exporter > should handle document with code blocks > __snapshots__/reactEmailExporterCodeBlock 1`] = `"

                            const hello = 'world';

                            console.log(hello);

                            "`; + +exports[`react email exporter > should handle document with complex nested structure > __snapshots__/reactEmailExporterComplexNested 1`] = `"

                            Complex Document

                            This is a paragraph with bold and italic text, plus a link.

                              List item with nested content

                              Nested paragraph

                              1. Nested numbered item

                            "`; + +exports[`react email exporter > should handle document with headings of different levels > __snapshots__/reactEmailExporterHeadings 1`] = `"

                            Heading 1

                            Heading 2

                            Heading 3

                            "`; + +exports[`react email exporter > should handle document with links > __snapshots__/reactEmailExporterWithLinks 1`] = `"

                            Click here

                            "`; + +exports[`react email exporter > should handle document with mixed content types > __snapshots__/reactEmailExporterMixedContent 1`] = `"

                            Main Heading

                            Regular paragraph with bold text

                              Numbered list item

                            "`; + +exports[`react email exporter > should handle document with mixed list types > __snapshots__/reactEmailExporterMixedLists 1`] = `"

                              Bullet item 1

                              Bullet item 2

                              Numbered item 1

                              Numbered item 2

                            "`; + +exports[`react email exporter > should handle document with nested lists > __snapshots__/reactEmailExporterNestedLists 1`] = `"

                              Parent item

                              • Child item

                            "`; + +exports[`react email exporter > should handle document with only text blocks > __snapshots__/reactEmailExporterSimpleText 1`] = `"

                            Simple text content

                            "`; + +exports[`react email exporter > should handle document with styled text > __snapshots__/reactEmailExporterStyledText 1`] = `"

                            Bold and italic text

                            "`; + +exports[`react email exporter > should handle document with text alignment > __snapshots__/reactEmailExporterTextAlignment 1`] = `"

                            Center aligned text

                            Right aligned text

                            "`; + +exports[`react email exporter > should handle document with text colors > __snapshots__/reactEmailExporterTextColor 1`] = `"

                            Colored text

                            "`; + +exports[`react email exporter > should handle empty document > __snapshots__/reactEmailExporterEmpty 1`] = `"
                            "`; diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx index 058af57cfc..fba4ea7598 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -2,7 +2,16 @@ import { describe, it, expect } from "vitest"; import { render } from "@react-email/render"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; -import { BlockNoteSchema } from "@blocknote/core"; +import { + BlockNoteSchema, + createBlockSpec, + createInlineContentSpec, + createStyleSpec, + defaultBlockSpecs, + defaultInlineContentSpecs, + defaultStyleSpecs, + PageBreak, +} from "@blocknote/core"; import { testDocument } from "@shared/testDocument.js"; describe("react email exporter", () => { @@ -18,4 +27,758 @@ describe("react email exporter", () => { const html = await render(reactElement); expect(html).toMatchSnapshot("__snapshots__/reactEmailExporter"); }); + + it("typescript: schema with extra block", async () => { + const schema = BlockNoteSchema.create({ + blockSpecs: { + ...defaultBlockSpecs, + pageBreak: PageBreak, + extraBlock: createBlockSpec( + { + content: "none", + type: "extraBlock", + propSchema: {}, + }, + {} as any, + ), + }, + }); + + new ReactEmailExporter( + schema, + // @ts-expect-error + reactEmailDefaultSchemaMappings, + ); + + new ReactEmailExporter(schema, { + // @ts-expect-error + blockMapping: reactEmailDefaultSchemaMappings.blockMapping, + inlineContentMapping: + reactEmailDefaultSchemaMappings.inlineContentMapping, + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, + }); + + new ReactEmailExporter(schema, { + blockMapping: { + ...reactEmailDefaultSchemaMappings.blockMapping, + extraBlock: (_b, _t) => { + throw new Error("extraBlock not implemented"); + }, + }, + inlineContentMapping: + reactEmailDefaultSchemaMappings.inlineContentMapping, + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, + }); + }); + + it("typescript: schema with extra inline content", async () => { + const schema = BlockNoteSchema.create({ + inlineContentSpecs: { + ...defaultInlineContentSpecs, + extraInlineContent: createInlineContentSpec( + { + type: "extraInlineContent", + content: "styled", + propSchema: {}, + }, + {} as any, + ), + }, + }); + + new ReactEmailExporter( + schema, + // @ts-expect-error + reactEmailDefaultSchemaMappings, + ); + + new ReactEmailExporter(schema, { + blockMapping: reactEmailDefaultSchemaMappings.blockMapping, + // @ts-expect-error + inlineContentMapping: + reactEmailDefaultSchemaMappings.inlineContentMapping, + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, + }); + + // no error + new ReactEmailExporter(schema, { + blockMapping: reactEmailDefaultSchemaMappings.blockMapping, + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, + inlineContentMapping: { + ...reactEmailDefaultSchemaMappings.inlineContentMapping, + extraInlineContent: () => { + throw new Error("extraInlineContent not implemented"); + }, + }, + }); + }); + + it("typescript: schema with extra style", async () => { + const schema = BlockNoteSchema.create({ + styleSpecs: { + ...defaultStyleSpecs, + extraStyle: createStyleSpec( + { + type: "extraStyle", + propSchema: "boolean", + }, + {} as any, + ), + }, + }); + + new ReactEmailExporter( + schema, + // @ts-expect-error + reactEmailDefaultSchemaMappings, + ); + + new ReactEmailExporter(schema, { + blockMapping: reactEmailDefaultSchemaMappings.blockMapping, + inlineContentMapping: + reactEmailDefaultSchemaMappings.inlineContentMapping, + // @ts-expect-error + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, + }); + + // no error + new ReactEmailExporter(schema, { + blockMapping: reactEmailDefaultSchemaMappings.blockMapping, + inlineContentMapping: + reactEmailDefaultSchemaMappings.inlineContentMapping, + styleMapping: { + ...reactEmailDefaultSchemaMappings.styleMapping, + extraStyle: () => { + throw new Error("extraStyle not implemented"); + }, + }, + }); + }); + + it("should export a document with preview", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create({ + blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak }, + }), + reactEmailDefaultSchemaMappings, + ); + + const html = await exporter.toReactEmailDocument(testDocument as any, { + preview: "This is a preview of the email content", + }); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterWithPreview"); + }); + + it("should export a document with multiple preview lines", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create({ + blockSpecs: { ...defaultBlockSpecs, pageBreak: PageBreak }, + }), + reactEmailDefaultSchemaMappings, + ); + + const html = await exporter.toReactEmailDocument(testDocument as any, { + preview: ["First preview line", "Second preview line"], + }); + expect(html).toMatchSnapshot( + "__snapshots__/reactEmailExporterWithMultiplePreview", + ); + }); + + it("should handle empty document", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const html = await exporter.toReactEmailDocument([]); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterEmpty"); + }); + + it("should handle document with only text blocks", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const simpleDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "text", + text: "Simple text content", + styles: {}, + }, + ], + children: [], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(simpleDocument as any); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterSimpleText"); + }); + + it("should handle document with styled text", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const styledDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "text", + text: "Bold and italic text", + styles: { + bold: true, + italic: true, + }, + }, + ], + children: [], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(styledDocument as any); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterStyledText"); + }); + + it("should handle document with links", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const linkDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "link", + href: "https://example.com", + content: [ + { + type: "text", + text: "Click here", + styles: {}, + }, + ], + }, + ], + children: [], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(linkDocument as any); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterWithLinks"); + }); + + it("should handle document with nested lists", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const nestedListDocument = [ + { + id: "1", + type: "bulletListItem", + content: [ + { + type: "text", + text: "Parent item", + styles: {}, + }, + ], + children: [ + { + id: "2", + type: "bulletListItem", + content: [ + { + type: "text", + text: "Child item", + styles: {}, + }, + ], + children: [], + props: {}, + }, + ], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(nestedListDocument as any); + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterNestedLists"); + }); + + it("should handle document with mixed content types", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const mixedDocument = [ + { + id: "1", + type: "heading", + content: [ + { + type: "text", + text: "Main Heading", + styles: {}, + }, + ], + children: [], + props: { level: 1 }, + }, + { + id: "2", + type: "paragraph", + content: [ + { + type: "text", + text: "Regular paragraph with ", + styles: {}, + }, + { + type: "text", + text: "bold text", + styles: { bold: true }, + }, + ], + children: [], + props: {}, + }, + { + id: "3", + type: "numberedListItem", + content: [ + { + type: "text", + text: "Numbered list item", + styles: {}, + }, + ], + children: [], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(mixedDocument as any); + expect(html).toMatchSnapshot( + "__snapshots__/reactEmailExporterMixedContent", + ); + }); + + it("should handle document with text alignment", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const alignedDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "text", + text: "Center aligned text", + styles: {}, + }, + ], + children: [], + props: { textAlignment: "center" }, + }, + { + id: "2", + type: "paragraph", + content: [ + { + type: "text", + text: "Right aligned text", + styles: {}, + }, + ], + children: [], + props: { textAlignment: "right" }, + }, + ]; + + const html = await exporter.toReactEmailDocument(alignedDocument as any); + expect(html).toMatchSnapshot( + "__snapshots__/reactEmailExporterTextAlignment", + ); + }); + + it("should handle document with background colors", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const coloredDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "text", + text: "Text with background color", + styles: {}, + }, + ], + children: [], + props: { backgroundColor: "blue" }, + }, + ]; + + const html = await exporter.toReactEmailDocument(coloredDocument as any); + + expect(html).toMatchSnapshot( + "__snapshots__/reactEmailExporterBackgroundColor", + ); + }); + + it("should handle document with text colors", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const coloredDocument = [ + { + id: "1", + type: "paragraph", + content: [ + { + type: "text", + text: "Colored text", + styles: {}, + }, + ], + children: [], + props: { textColor: "red" }, + }, + ]; + + const html = await exporter.toReactEmailDocument(coloredDocument as any); + + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterTextColor"); + }); + + it("should handle document with code blocks", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const codeDocument = [ + { + id: "1", + type: "codeBlock", + content: [ + { + type: "text", + text: "const hello = 'world';\nconsole.log(hello);", + styles: {}, + }, + ], + children: [], + props: { language: "javascript" }, + }, + ]; + + const html = await exporter.toReactEmailDocument(codeDocument as any); + + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterCodeBlock"); + }); + + it("should handle document with check list items", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const checkListDocument = [ + { + id: "1", + type: "checkListItem", + content: [ + { + type: "text", + text: "Checked item", + styles: {}, + }, + ], + children: [], + props: { checked: true }, + }, + { + id: "2", + type: "checkListItem", + content: [ + { + type: "text", + text: "Unchecked item", + styles: {}, + }, + ], + children: [], + props: { checked: false }, + }, + ]; + + const html = await exporter.toReactEmailDocument(checkListDocument as any); + + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterCheckList"); + }); + + it("should handle document with headings of different levels", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const headingDocument = [ + { + id: "1", + type: "heading", + content: [ + { + type: "text", + text: "Heading 1", + styles: {}, + }, + ], + children: [], + props: { level: 1 }, + }, + { + id: "2", + type: "heading", + content: [ + { + type: "text", + text: "Heading 2", + styles: {}, + }, + ], + children: [], + props: { level: 2 }, + }, + { + id: "3", + type: "heading", + content: [ + { + type: "text", + text: "Heading 3", + styles: {}, + }, + ], + children: [], + props: { level: 3 }, + }, + ]; + + const html = await exporter.toReactEmailDocument(headingDocument as any); + + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterHeadings"); + }); + + it("should handle document with complex nested structure", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const complexDocument = [ + { + id: "1", + type: "heading", + content: [ + { + type: "text", + text: "Complex Document", + styles: { bold: true }, + }, + ], + children: [], + props: { level: 1 }, + }, + { + id: "2", + type: "paragraph", + content: [ + { + type: "text", + text: "This is a paragraph with ", + styles: {}, + }, + { + type: "text", + text: "bold", + styles: { bold: true }, + }, + { + type: "text", + text: " and ", + styles: {}, + }, + { + type: "text", + text: "italic", + styles: { italic: true }, + }, + { + type: "text", + text: " text, plus a ", + styles: {}, + }, + { + type: "link", + href: "https://example.com", + content: [ + { + type: "text", + text: "link", + styles: {}, + }, + ], + }, + { + type: "text", + text: ".", + styles: {}, + }, + ], + children: [], + props: { textAlignment: "center" }, + }, + { + id: "3", + type: "bulletListItem", + content: [ + { + type: "text", + text: "List item with nested content", + styles: {}, + }, + ], + children: [ + { + id: "4", + type: "paragraph", + content: [ + { + type: "text", + text: "Nested paragraph", + styles: {}, + }, + ], + children: [], + props: {}, + }, + { + id: "5", + type: "numberedListItem", + content: [ + { + type: "text", + text: "Nested numbered item", + styles: {}, + }, + ], + children: [], + props: {}, + }, + ], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(complexDocument as any); + + expect(html).toMatchSnapshot( + "__snapshots__/reactEmailExporterComplexNested", + ); + }); + + it("should handle document with mixed list types", async () => { + const exporter = new ReactEmailExporter( + BlockNoteSchema.create(), + reactEmailDefaultSchemaMappings, + ); + + const mixedListDocument = [ + { + id: "1", + type: "bulletListItem", + content: [ + { + type: "text", + text: "Bullet item 1", + styles: {}, + }, + ], + children: [], + props: {}, + }, + { + id: "2", + type: "bulletListItem", + content: [ + { + type: "text", + text: "Bullet item 2", + styles: {}, + }, + ], + children: [], + props: {}, + }, + { + id: "3", + type: "numberedListItem", + content: [ + { + type: "text", + text: "Numbered item 1", + styles: {}, + }, + ], + children: [], + props: {}, + }, + { + id: "4", + type: "numberedListItem", + content: [ + { + type: "text", + text: "Numbered item 2", + styles: {}, + }, + ], + children: [], + props: {}, + }, + ]; + + const html = await exporter.toReactEmailDocument(mixedListDocument as any); + + expect(html).toMatchSnapshot("__snapshots__/reactEmailExporterMixedLists"); + }); }); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index ff13edcc15..991c00a1b9 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -6,6 +6,7 @@ export default defineWorkspace([ "./packages/shadcn/vite.config.ts", "./packages/server-util/vite.config.ts", "./packages/xl-pdf-exporter/vite.config.ts", + "./packages/xl-react-email-exporter/vite.config.ts", "./packages/xl-ai/vite.config.ts", "./packages/mantine/vite.config.ts", "./packages/core/vite.config.ts", From 4b9fc39a40c450970d571eb502b365c8214b6c36 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 12:15:43 +0200 Subject: [PATCH 34/38] refactor: rename to xl-email-exporter --- .../.bnexample.json | 8 +- .../App.tsx | 7 +- .../package.json | 4 +- .../.gitignore | 0 .../package.json | 12 +- .../src/index.ts | 0 .../reactEmailExporter.test.tsx.snap | 2 +- .../src/react-email/defaultSchema/blocks.tsx | 0 .../src/react-email/defaultSchema/index.ts | 0 .../defaultSchema/inlinecontent.tsx | 0 .../src/react-email/defaultSchema/styles.tsx | 0 .../src/react-email/index.ts | 0 .../react-email/reactEmailExporter.test.tsx | 6 +- .../src/react-email/reactEmailExporter.tsx | 29 +- .../src/vite-env.d.ts | 0 .../tsconfig.json | 0 .../vite.config.ts | 0 .../viteSetup.ts | 0 .../vitestSetup.ts | 0 playground/package.json | 4 +- playground/src/examples.gen.tsx | 2778 ++++++++--------- playground/vite.config.ts | 2 +- pnpm-lock.yaml | 138 +- vitest.workspace.ts | 2 +- 24 files changed, 1439 insertions(+), 1553 deletions(-) rename packages/{xl-react-email-exporter => xl-email-exporter}/.gitignore (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/package.json (84%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/index.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap (71%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/defaultSchema/blocks.tsx (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/defaultSchema/index.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/defaultSchema/inlinecontent.tsx (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/defaultSchema/styles.tsx (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/index.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/reactEmailExporter.test.tsx (99%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/react-email/reactEmailExporter.tsx (90%) rename packages/{xl-react-email-exporter => xl-email-exporter}/src/vite-env.d.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/tsconfig.json (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/vite.config.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/viteSetup.ts (100%) rename packages/{xl-react-email-exporter => xl-email-exporter}/vitestSetup.ts (100%) diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json index 2ffb362d31..4292fc5c25 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/.bnexample.json @@ -2,12 +2,10 @@ "playground": true, "docs": true, "author": "jmarbutt", - "tags": [ - "" - ], + "tags": [""], "dependencies": { - "@blocknote/xl-react-email-exporter": "latest", + "@blocknote/xl-email-exporter": "latest", "@react-email/render": "^1.1.2" }, "pro": true -} \ No newline at end of file +} diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx index 18dcef6feb..c74855be0e 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/App.tsx @@ -10,13 +10,12 @@ import "@blocknote/mantine/style.css"; import { SuggestionMenuController, getDefaultReactSlashMenuItems, - getPageBreakReactSlashMenuItems, useCreateBlockNote, } from "@blocknote/react"; import { ReactEmailExporter, reactEmailDefaultSchemaMappings, -} from "@blocknote/xl-react-email-exporter"; +} from "@blocknote/xl-email-exporter"; import { useEffect, useMemo, useState } from "react"; import "./styles.css"; @@ -316,7 +315,9 @@ export default function App() { }); const onChange = async () => { - if (!editor || !editor.document) return; + if (!editor || !editor.document) { + return; + } const exporter = new ReactEmailExporter( editor.schema, reactEmailDefaultSchemaMappings, diff --git a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json index 094c2f5672..39fce8c55c 100644 --- a/examples/05-interoperability/08-converting-blocks-to-react-email/package.json +++ b/examples/05-interoperability/08-converting-blocks-to-react-email/package.json @@ -17,7 +17,7 @@ "@blocknote/shadcn": "latest", "react": "^18.3.1", "react-dom": "^18.3.1", - "@blocknote/xl-react-email-exporter": "latest", + "@blocknote/xl-email-exporter": "latest", "@react-email/render": "^1.1.2" }, "devDependencies": { @@ -26,4 +26,4 @@ "@vitejs/plugin-react": "^4.3.1", "vite": "^5.3.4" } -} \ No newline at end of file +} diff --git a/packages/xl-react-email-exporter/.gitignore b/packages/xl-email-exporter/.gitignore similarity index 100% rename from packages/xl-react-email-exporter/.gitignore rename to packages/xl-email-exporter/.gitignore diff --git a/packages/xl-react-email-exporter/package.json b/packages/xl-email-exporter/package.json similarity index 84% rename from packages/xl-react-email-exporter/package.json rename to packages/xl-email-exporter/package.json index 35b4ca18fc..2e55ceef71 100644 --- a/packages/xl-react-email-exporter/package.json +++ b/packages/xl-email-exporter/package.json @@ -1,12 +1,12 @@ { - "name": "@blocknote/xl-react-email-exporter", + "name": "@blocknote/xl-email-exporter", "homepage": "https://github.com/TypeCellOS/BlockNote", "private": false, "sideEffects": false, "repository": { "type": "git", "url": "git+https://github.com/TypeCellOS/BlockNote.git", - "directory": "packages/xl-react-email-exporter" + "directory": "packages/xl-email-exporter" }, "license": "AGPL-3.0 OR PROPRIETARY", "version": "0.31.3", @@ -32,13 +32,13 @@ "type": "module", "source": "src/index.ts", "types": "./types/src/index.d.ts", - "main": "./dist/blocknote-xl-react-email-exporter.umd.cjs", - "module": "./dist/blocknote-xl-react-email-exporter.js", + "main": "./dist/blocknote-xl-email-exporter.umd.cjs", + "module": "./dist/blocknote-xl-email-exporter.js", "exports": { ".": { "types": "./types/src/index.d.ts", - "import": "./dist/blocknote-xl-react-email-exporter.js", - "require": "./dist/blocknote-xl-react-email-exporter.umd.cjs" + "import": "./dist/blocknote-xl-email-exporter.js", + "require": "./dist/blocknote-xl-email-exporter.umd.cjs" }, "./style.css": { "import": "./dist/style.css", diff --git a/packages/xl-react-email-exporter/src/index.ts b/packages/xl-email-exporter/src/index.ts similarity index 100% rename from packages/xl-react-email-exporter/src/index.ts rename to packages/xl-email-exporter/src/index.ts diff --git a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap b/packages/xl-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap similarity index 71% rename from packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap rename to packages/xl-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap index 30c707c8d3..ca755fccf5 100644 --- a/packages/xl-react-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap +++ b/packages/xl-email-exporter/src/react-email/__snapshots__/reactEmailExporter.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`react email exporter > should export a document (HTML snapshot) > __snapshots__/reactEmailExporter 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><html dir="ltr" lang="en"><head><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="font-family:&#x27;SF Pro Display&#x27;, -apple-system, BlinkMacSystemFont, &#x27;Segoe UI&#x27;, &#x27;Roboto&#x27;, &#x27;Helvetica Neue&#x27;, Arial, sans-serif;font-size:16px;line-height:1.5;color:#333"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-style:italic">Welcome to this </span><span style="font-style:italic;font-weight:bold">demo 🙌!</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World nested</span></p></td></tr></tbody></table><div style="margin-left:24px"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Hello World double nested</span></p></td></tr></tbody></table></div></div><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left;background-color:#fbe4e4"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">This paragraph has a background color</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Paragraph</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><h1><span>Heading</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><h1><span>Heading right</span></h1></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:justify"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><hr style="border:none;border-top:2px dashed #ccc;margin:24px 0"/></td></tr></tbody></table><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p><ul style="margin-bottom:0.5rem;list-style-type:disc;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li><li style="text-align:right;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</span></p></li></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item 2</span></p><div style="margin-top:8px"><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 1</span></p></li><li style="text-align:left"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested 2</span></p></li><li style="text-align:right;background-color:#fbe4e4;color:#0b6e99;align-items:flex-end"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky right</span></p></li><li style="text-align:center;background-color:#fbe4e4;color:#0b6e99;align-items:center"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item Nested funky center</span></p></li></ol></div></li></ol></ul><ol start="1" style="margin-bottom:0.5rem;list-style-type:decimal;padding-left:1.5rem"><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span>Numbered List Item</span></p></ol><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><svg width="18" height="18" viewBox="0 0 18 18" style="display:inline;vertical-align:middle;margin-right:8px"><rect x="2" y="2" width="14" height="14" rx="3" fill="#fff" stroke="#888" stroke-width="2"></rect></svg><span><span>Check List Item</span></span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Wide Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#888" style="display:inline;vertical-align:middle"><path d="M3 8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8ZM10 4V9H5V20H19V4H10Z"></path></svg><span style="vertical-align:middle">Open file</span></a></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><img alt="From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:right;align-items:flex-end"><tbody><tr><td><img alt="" src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg" style="display:block;outline:none;border:none;text-decoration:none" width="200"/></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#1976D2" style="display:inline;vertical-align:middle"><path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM8 5V19H16V5H8ZM4 5V7H6V5H4ZM18 5V7H20V5H18ZM4 9V11H6V9H4ZM18 9V11H20V9H18ZM4 13V15H6V13H4ZM18 13V15H20V13H18ZM4 17V19H6V17H4ZM18 17V19H20V17H18Z"></path></svg><span style="vertical-align:middle">Open video file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">Open audio file</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><div style="margin:8px 0"><a href="" style="color:#333;text-decoration-line:none;text-decoration:none;display:inline-flex;align-items:center;gap:8px;font-size:16px" target="_blank"><svg height="18" width="18" viewBox="0 0 24 24" fill="#4F8A10" style="display:inline;vertical-align:middle"><path d="M2 16.0001H5.88889L11.1834 20.3319C11.2727 20.405 11.3846 20.4449 11.5 20.4449C11.7761 20.4449 12 20.2211 12 19.9449V4.05519C12 3.93977 11.9601 3.8279 11.887 3.73857C11.7121 3.52485 11.3971 3.49335 11.1834 3.66821L5.88889 8.00007H2C1.44772 8.00007 1 8.44778 1 9.00007V15.0001C1 15.5524 1.44772 16.0001 2 16.0001ZM23 12C23 15.292 21.5539 18.2463 19.2622 20.2622L17.8445 18.8444C19.7758 17.1937 21 14.7398 21 12C21 9.26016 19.7758 6.80629 17.8445 5.15557L19.2622 3.73779C21.5539 5.75368 23 8.70795 23 12ZM18 12C18 10.0883 17.106 8.38548 15.7133 7.28673L14.2842 8.71584C15.3213 9.43855 16 10.64 16 12C16 13.36 15.3213 14.5614 14.2842 15.2841L15.7133 16.7132C17.106 15.6145 18 13.9116 18 12Z"></path></svg><span style="vertical-align:middle">audio.mp3</span></a><p style="font-size:13px;line-height:24px;color:#888;margin:4px 0 0 0;margin-top:4px;margin-right:0;margin-bottom:0;margin-left:0">Audio file caption</p></div></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold">Inline Content:</span></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:left"><tbody><tr><td><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px"><span style="font-weight:bold;font-style:italic;color:red;background-color:blue">Styled Text</span><span> </span><a href="https://www.blocknotejs.org" style="color:#067df7;text-decoration-line:none" target="_blank"><span>Link</span></a></p></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table style="border-collapse:collapse;width:100%;margin:16px 0;border:1px solid #ddd;border-radius:4px;overflow:hidden" border="0" cellPadding="0" cellSpacing="0"><tbody><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 1</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 2</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 3</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 4</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span style="font-weight:bold">Table Cell Bold 5</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 6</span></td></tr><tr><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 7</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 8</span></td><td style="border:1px solid #ddd;padding:8px 12px;background:#fff;font-weight:normal;text-align:left;color:inherit"><span>Table Cell 9</span></td></tr></tbody></table></td></tr></tbody></table><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><pre style="color:#f8f8f2;background:#282a36;text-shadow:0 1px rgba(0, 0, 0, 0.3);font-family:Consolas, Monaco, &#x27;Andale Mono&#x27;, &#x27;Ubuntu Mono&#x27;, monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;border-radius:0.3em;width:100%"><code><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#8be9fd">const</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">helloWorld</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace">message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">=&gt;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">{</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace"> </span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">console</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">.</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f1fa8c">log</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">(</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#50fa7b">&quot;Hello World&quot;</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">,</span><span style="font-family:&#x27;CommitMono&#x27;, monospace"> message</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">)</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p><p style="margin:0;min-height:1em"><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">}</span><span style="font-family:&#x27;CommitMono&#x27;, monospace;color:#f8f8f2">;</span></p></code></pre></td></tr></tbody></table></td></tr></tbody></table></body></html><!--/$-->"`; +exports[`react email exporter > should export a document (HTML snapshot) > __snapshots__/reactEmailExporter 1`] = `"

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; exports[`react email exporter > should export a document with multiple preview lines > __snapshots__/reactEmailExporterWithMultiplePreview 1`] = `"
                            First preview lineSecond preview line
                             ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏

                            Welcome to this demo 🙌!

                            Hello World nested

                            Hello World double nested

                            This paragraph has a background color

                            Paragraph

                            Heading

                            Heading right

                            justified paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.


                              Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              • Bullet List Item right. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

                              1. Numbered List Item 1

                              2. Numbered List Item 2

                                1. Numbered List Item Nested 1

                                2. Numbered List Item Nested 2

                                3. Numbered List Item Nested funky right

                                4. Numbered List Item Nested funky center

                              Numbered List Item

                            Check List Item

                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            Wide CellTable CellTable Cell
                            From https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg
                            Open video file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm

                            Open audio file

                            From https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3

                            audio.mp3

                            Audio file caption

                            Inline Content:

                            Styled Text Link

                            Table Cell 1Table Cell 2Table Cell 3
                            Table Cell 4Table Cell Bold 5Table Cell 6
                            Table Cell 7Table Cell 8Table Cell 9

                            const helloWorld = (message) => {

                            console.log("Hello World", message);

                            };

                            "`; diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx similarity index 100% rename from packages/xl-react-email-exporter/src/react-email/defaultSchema/blocks.tsx rename to packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts b/packages/xl-email-exporter/src/react-email/defaultSchema/index.ts similarity index 100% rename from packages/xl-react-email-exporter/src/react-email/defaultSchema/index.ts rename to packages/xl-email-exporter/src/react-email/defaultSchema/index.ts diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx similarity index 100% rename from packages/xl-react-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx rename to packages/xl-email-exporter/src/react-email/defaultSchema/inlinecontent.tsx diff --git a/packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/styles.tsx similarity index 100% rename from packages/xl-react-email-exporter/src/react-email/defaultSchema/styles.tsx rename to packages/xl-email-exporter/src/react-email/defaultSchema/styles.tsx diff --git a/packages/xl-react-email-exporter/src/react-email/index.ts b/packages/xl-email-exporter/src/react-email/index.ts similarity index 100% rename from packages/xl-react-email-exporter/src/react-email/index.ts rename to packages/xl-email-exporter/src/react-email/index.ts diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx similarity index 99% rename from packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx rename to packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx index fba4ea7598..e23261d5c2 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,5 +1,4 @@ import { describe, it, expect } from "vitest"; -import { render } from "@react-email/render"; import { ReactEmailExporter } from "./reactEmailExporter"; import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; import { @@ -21,10 +20,7 @@ describe("react email exporter", () => { reactEmailDefaultSchemaMappings, ); - const reactElement = await exporter.toReactEmailDocument( - testDocument as any, - ); - const html = await render(reactElement); + const html = await exporter.toReactEmailDocument(testDocument as any); expect(html).toMatchSnapshot("__snapshots__/reactEmailExporter"); }); diff --git a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx similarity index 90% rename from packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx rename to packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx index a1427a5ff9..b85144f50a 100644 --- a/packages/xl-react-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx @@ -258,13 +258,34 @@ export class ReactEmailExporter< public async toReactEmailDocument( blocks: Block[], options?: { + /** + * Inject elements into the {@link Head} element + * @see https://react.email/docs/components/head + */ + head?: React.ReactElement; + /** + * Set the preview text for the email + * @see https://react.email/docs/components/preview + */ preview?: string | string[]; + /** + * Add a header to every page. + * The React component passed must be a React-Email component + * @see https://react.email/components + */ + header?: React.ReactElement; + /** + * Add a footer to every page. + * The React component passed must be a React-Email component + * @see https://react.email/components + */ + footer?: React.ReactElement; }, ) { const transformedBlocks = await this.transformBlocks(blocks); return renderEmail( - + {options?.head} {options?.preview && {options.preview}} - {transformedBlocks} + + {options?.header} + {transformedBlocks} + {options?.footer} + , diff --git a/packages/xl-react-email-exporter/src/vite-env.d.ts b/packages/xl-email-exporter/src/vite-env.d.ts similarity index 100% rename from packages/xl-react-email-exporter/src/vite-env.d.ts rename to packages/xl-email-exporter/src/vite-env.d.ts diff --git a/packages/xl-react-email-exporter/tsconfig.json b/packages/xl-email-exporter/tsconfig.json similarity index 100% rename from packages/xl-react-email-exporter/tsconfig.json rename to packages/xl-email-exporter/tsconfig.json diff --git a/packages/xl-react-email-exporter/vite.config.ts b/packages/xl-email-exporter/vite.config.ts similarity index 100% rename from packages/xl-react-email-exporter/vite.config.ts rename to packages/xl-email-exporter/vite.config.ts diff --git a/packages/xl-react-email-exporter/viteSetup.ts b/packages/xl-email-exporter/viteSetup.ts similarity index 100% rename from packages/xl-react-email-exporter/viteSetup.ts rename to packages/xl-email-exporter/viteSetup.ts diff --git a/packages/xl-react-email-exporter/vitestSetup.ts b/packages/xl-email-exporter/vitestSetup.ts similarity index 100% rename from packages/xl-react-email-exporter/vitestSetup.ts rename to packages/xl-email-exporter/vitestSetup.ts diff --git a/playground/package.json b/playground/package.json index fc177a4382..e858426705 100644 --- a/playground/package.json +++ b/playground/package.json @@ -30,7 +30,7 @@ "@blocknote/xl-multi-column": "workspace:^", "@blocknote/xl-odt-exporter": "workspace:^", "@blocknote/xl-pdf-exporter": "workspace:^", - "@blocknote/xl-react-email-exporter": "workspace:^", + "@blocknote/xl-email-exporter": "workspace:^", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@liveblocks/core": "^2.23.1", @@ -82,4 +82,4 @@ "../.eslintrc.json" ] } -} \ No newline at end of file +} diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 399008e871..39b3880f4b 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1,586 +1,533 @@ // generated by dev-scripts/examples/gen.ts - export const examples = { - "basic": { - "pathFromRoot": "examples/01-basic", - "slug": "basic", - "projects": [ - { - "projectSlug": "minimal", - "fullSlug": "basic/minimal", - "pathFromRoot": "examples/01-basic/01-minimal", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic" - ] - }, - "title": "Basic Setup", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "block-objects", - "fullSlug": "basic/block-objects", - "pathFromRoot": "examples/01-basic/02-block-objects", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic", - "Blocks", - "Inline Content" - ] - }, - "title": "Displaying Document JSON", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "multi-column", - "fullSlug": "basic/multi-column", - "pathFromRoot": "examples/01-basic/03-multi-column", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic", - "Blocks" - ], - "dependencies": { - "@blocknote/xl-multi-column": "latest" +export const examples = { + basic: { + pathFromRoot: "examples/01-basic", + slug: "basic", + projects: [ + { + projectSlug: "minimal", + fullSlug: "basic/minimal", + pathFromRoot: "examples/01-basic/01-minimal", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic"], + }, + title: "Basic Setup", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "block-objects", + fullSlug: "basic/block-objects", + pathFromRoot: "examples/01-basic/02-block-objects", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic", "Blocks", "Inline Content"], + }, + title: "Displaying Document JSON", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "multi-column", + fullSlug: "basic/multi-column", + pathFromRoot: "examples/01-basic/03-multi-column", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic", "Blocks"], + dependencies: { + "@blocknote/xl-multi-column": "latest", } as any, - "pro": true - }, - "title": "Multi-Column Blocks", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "default-blocks", - "fullSlug": "basic/default-blocks", - "pathFromRoot": "examples/01-basic/04-default-blocks", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic", - "Blocks", - "Inline Content" - ] - }, - "title": "Default Schema Showcase", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "removing-default-blocks", - "fullSlug": "basic/removing-default-blocks", - "pathFromRoot": "examples/01-basic/05-removing-default-blocks", - "config": { - "playground": true, - "docs": true, - "author": "hunxjunedo", - "tags": [ - "Basic", - "removing", - "blocks" - ] - }, - "title": "Removing Default Blocks from Schema", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "block-manipulation", - "fullSlug": "basic/block-manipulation", - "pathFromRoot": "examples/01-basic/06-block-manipulation", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Blocks" - ] - }, - "title": "Manipulating Blocks", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "selection-blocks", - "fullSlug": "basic/selection-blocks", - "pathFromRoot": "examples/01-basic/07-selection-blocks", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Blocks" - ] - }, - "title": "Displaying Selected Blocks", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "ariakit", - "fullSlug": "basic/ariakit", - "pathFromRoot": "examples/01-basic/08-ariakit", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic" - ] - }, - "title": "Use with Ariakit", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "shadcn", - "fullSlug": "basic/shadcn", - "pathFromRoot": "examples/01-basic/09-shadcn", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic" - ] - }, - "title": "Use with ShadCN", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "localization", - "fullSlug": "basic/localization", - "pathFromRoot": "examples/01-basic/10-localization", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic" - ] - }, - "title": "Localization (i18n)", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "custom-placeholder", - "fullSlug": "basic/custom-placeholder", - "pathFromRoot": "examples/01-basic/11-custom-placeholder", - "config": { - "playground": true, - "docs": true, - "author": "ezhil56x", - "tags": [ - "Basic" - ] - }, - "title": "Change placeholder text", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "multi-editor", - "fullSlug": "basic/multi-editor", - "pathFromRoot": "examples/01-basic/12-multi-editor", - "config": { - "playground": true, - "docs": true, - "author": "areknawo", - "tags": [ - "Basic" - ] - }, - "title": "Multi-Editor Setup", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "custom-paste-handler", - "fullSlug": "basic/custom-paste-handler", - "pathFromRoot": "examples/01-basic/13-custom-paste-handler", - "config": { - "playground": true, - "docs": true, - "author": "nperez0111", - "tags": [ - "Basic" - ] - }, - "title": "Custom Paste Handler", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - }, - { - "projectSlug": "testing", - "fullSlug": "basic/testing", - "pathFromRoot": "examples/01-basic/testing", - "config": { - "playground": true, - "docs": false - }, - "title": "Test Editor", - "group": { - "pathFromRoot": "examples/01-basic", - "slug": "basic" - } - } - ] + pro: true, + }, + title: "Multi-Column Blocks", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "default-blocks", + fullSlug: "basic/default-blocks", + pathFromRoot: "examples/01-basic/04-default-blocks", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic", "Blocks", "Inline Content"], + }, + title: "Default Schema Showcase", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "removing-default-blocks", + fullSlug: "basic/removing-default-blocks", + pathFromRoot: "examples/01-basic/05-removing-default-blocks", + config: { + playground: true, + docs: true, + author: "hunxjunedo", + tags: ["Basic", "removing", "blocks"], + }, + title: "Removing Default Blocks from Schema", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "block-manipulation", + fullSlug: "basic/block-manipulation", + pathFromRoot: "examples/01-basic/06-block-manipulation", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Blocks"], + }, + title: "Manipulating Blocks", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "selection-blocks", + fullSlug: "basic/selection-blocks", + pathFromRoot: "examples/01-basic/07-selection-blocks", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Blocks"], + }, + title: "Displaying Selected Blocks", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "ariakit", + fullSlug: "basic/ariakit", + pathFromRoot: "examples/01-basic/08-ariakit", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic"], + }, + title: "Use with Ariakit", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "shadcn", + fullSlug: "basic/shadcn", + pathFromRoot: "examples/01-basic/09-shadcn", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic"], + }, + title: "Use with ShadCN", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "localization", + fullSlug: "basic/localization", + pathFromRoot: "examples/01-basic/10-localization", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic"], + }, + title: "Localization (i18n)", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "custom-placeholder", + fullSlug: "basic/custom-placeholder", + pathFromRoot: "examples/01-basic/11-custom-placeholder", + config: { + playground: true, + docs: true, + author: "ezhil56x", + tags: ["Basic"], + }, + title: "Change placeholder text", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "multi-editor", + fullSlug: "basic/multi-editor", + pathFromRoot: "examples/01-basic/12-multi-editor", + config: { + playground: true, + docs: true, + author: "areknawo", + tags: ["Basic"], + }, + title: "Multi-Editor Setup", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "custom-paste-handler", + fullSlug: "basic/custom-paste-handler", + pathFromRoot: "examples/01-basic/13-custom-paste-handler", + config: { + playground: true, + docs: true, + author: "nperez0111", + tags: ["Basic"], + }, + title: "Custom Paste Handler", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + { + projectSlug: "testing", + fullSlug: "basic/testing", + pathFromRoot: "examples/01-basic/testing", + config: { + playground: true, + docs: false, + }, + title: "Test Editor", + group: { + pathFromRoot: "examples/01-basic", + slug: "basic", + }, + }, + ], }, - "backend": { - "pathFromRoot": "examples/02-backend", - "slug": "backend", - "projects": [ - { - "projectSlug": "file-uploading", - "fullSlug": "backend/file-uploading", - "pathFromRoot": "examples/02-backend/01-file-uploading", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Intermediate", - "Saving/Loading" - ] - }, - "title": "Upload Files", - "group": { - "pathFromRoot": "examples/02-backend", - "slug": "backend" - } - }, - { - "projectSlug": "saving-loading", - "fullSlug": "backend/saving-loading", - "pathFromRoot": "examples/02-backend/02-saving-loading", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Intermediate", - "Blocks", - "Saving/Loading" - ] - }, - "title": "Saving & Loading", - "group": { - "pathFromRoot": "examples/02-backend", - "slug": "backend" - } - }, - { - "projectSlug": "s3", - "fullSlug": "backend/s3", - "pathFromRoot": "examples/02-backend/03-s3", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Intermediate", - "Saving/Loading" - ], - "dependencies": { + backend: { + pathFromRoot: "examples/02-backend", + slug: "backend", + projects: [ + { + projectSlug: "file-uploading", + fullSlug: "backend/file-uploading", + pathFromRoot: "examples/02-backend/01-file-uploading", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Intermediate", "Saving/Loading"], + }, + title: "Upload Files", + group: { + pathFromRoot: "examples/02-backend", + slug: "backend", + }, + }, + { + projectSlug: "saving-loading", + fullSlug: "backend/saving-loading", + pathFromRoot: "examples/02-backend/02-saving-loading", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Intermediate", "Blocks", "Saving/Loading"], + }, + title: "Saving & Loading", + group: { + pathFromRoot: "examples/02-backend", + slug: "backend", + }, + }, + { + projectSlug: "s3", + fullSlug: "backend/s3", + pathFromRoot: "examples/02-backend/03-s3", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Intermediate", "Saving/Loading"], + dependencies: { "@aws-sdk/client-s3": "^3.609.0", - "@aws-sdk/s3-request-presigner": "^3.609.0" + "@aws-sdk/s3-request-presigner": "^3.609.0", } as any, - "pro": true - }, - "title": "Upload Files to AWS S3", - "group": { - "pathFromRoot": "examples/02-backend", - "slug": "backend" - } - }, - { - "projectSlug": "rendering-static-documents", - "fullSlug": "backend/rendering-static-documents", - "pathFromRoot": "examples/02-backend/04-rendering-static-documents", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "server" - ], - "dependencies": { - "@blocknote/server-util": "latest" - } as any - }, - "title": "Rendering static documents", - "group": { - "pathFromRoot": "examples/02-backend", - "slug": "backend" - } - } - ] + pro: true, + }, + title: "Upload Files to AWS S3", + group: { + pathFromRoot: "examples/02-backend", + slug: "backend", + }, + }, + { + projectSlug: "rendering-static-documents", + fullSlug: "backend/rendering-static-documents", + pathFromRoot: "examples/02-backend/04-rendering-static-documents", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["server"], + dependencies: { + "@blocknote/server-util": "latest", + } as any, + }, + title: "Rendering static documents", + group: { + pathFromRoot: "examples/02-backend", + slug: "backend", + }, + }, + ], }, "ui-components": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components", - "projects": [ - { - "projectSlug": "formatting-toolbar-buttons", - "fullSlug": "ui-components/formatting-toolbar-buttons", - "pathFromRoot": "examples/03-ui-components/02-formatting-toolbar-buttons", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + projects: [ + { + projectSlug: "formatting-toolbar-buttons", + fullSlug: "ui-components/formatting-toolbar-buttons", + pathFromRoot: "examples/03-ui-components/02-formatting-toolbar-buttons", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Inline Content", "UI Components", - "Formatting Toolbar" - ] - }, - "title": "Adding Formatting Toolbar Buttons", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "formatting-toolbar-block-type-items", - "fullSlug": "ui-components/formatting-toolbar-block-type-items", - "pathFromRoot": "examples/03-ui-components/03-formatting-toolbar-block-type-items", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Intermediate", - "Blocks", - "UI Components", "Formatting Toolbar", - "Custom Schemas" - ], - "dependencies": { - "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1" - } as any - }, - "title": "Adding Block Type Select Items", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "side-menu-buttons", - "fullSlug": "ui-components/side-menu-buttons", - "pathFromRoot": "examples/03-ui-components/04-side-menu-buttons", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Intermediate", - "Blocks", - "UI Components", - "Block Side Menu" ], - "dependencies": { - "react-icons": "^5.2.1" - } as any - }, - "title": "Adding Block Side Menu Buttons", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "side-menu-drag-handle-items", - "fullSlug": "ui-components/side-menu-drag-handle-items", - "pathFromRoot": "examples/03-ui-components/05-side-menu-drag-handle-items", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + }, + title: "Adding Formatting Toolbar Buttons", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "formatting-toolbar-block-type-items", + fullSlug: "ui-components/formatting-toolbar-block-type-items", + pathFromRoot: + "examples/03-ui-components/03-formatting-toolbar-block-type-items", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Blocks", "UI Components", - "Block Side Menu" + "Formatting Toolbar", + "Custom Schemas", ], - "dependencies": { - "react-icons": "^5.2.1" - } as any - }, - "title": "Adding Drag Handle Menu Items", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "suggestion-menus-slash-menu-items", - "fullSlug": "ui-components/suggestion-menus-slash-menu-items", - "pathFromRoot": "examples/03-ui-components/06-suggestion-menus-slash-menu-items", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + dependencies: { + "@mantine/core": "^7.10.1", + "react-icons": "^5.2.1", + } as any, + }, + title: "Adding Block Type Select Items", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "side-menu-buttons", + fullSlug: "ui-components/side-menu-buttons", + pathFromRoot: "examples/03-ui-components/04-side-menu-buttons", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], + dependencies: { + "react-icons": "^5.2.1", + } as any, + }, + title: "Adding Block Side Menu Buttons", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "side-menu-drag-handle-items", + fullSlug: "ui-components/side-menu-drag-handle-items", + pathFromRoot: + "examples/03-ui-components/05-side-menu-drag-handle-items", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], + dependencies: { + "react-icons": "^5.2.1", + } as any, + }, + title: "Adding Drag Handle Menu Items", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "suggestion-menus-slash-menu-items", + fullSlug: "ui-components/suggestion-menus-slash-menu-items", + pathFromRoot: + "examples/03-ui-components/06-suggestion-menus-slash-menu-items", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Slash Menu" + "Slash Menu", ], - "dependencies": { - "react-icons": "^5.2.1" - } as any - }, - "title": "Adding Slash Menu Items", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "suggestion-menus-slash-menu-component", - "fullSlug": "ui-components/suggestion-menus-slash-menu-component", - "pathFromRoot": "examples/03-ui-components/07-suggestion-menus-slash-menu-component", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + dependencies: { + "react-icons": "^5.2.1", + } as any, + }, + title: "Adding Slash Menu Items", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "suggestion-menus-slash-menu-component", + fullSlug: "ui-components/suggestion-menus-slash-menu-component", + pathFromRoot: + "examples/03-ui-components/07-suggestion-menus-slash-menu-component", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "UI Components", "Suggestion Menus", "Slash Menu", - "Appearance & Styling" - ] - }, - "title": "Replacing Slash Menu Component", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "suggestion-menus-emoji-picker-columns", - "fullSlug": "ui-components/suggestion-menus-emoji-picker-columns", - "pathFromRoot": "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + "Appearance & Styling", + ], + }, + title: "Replacing Slash Menu Component", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "suggestion-menus-emoji-picker-columns", + fullSlug: "ui-components/suggestion-menus-emoji-picker-columns", + pathFromRoot: + "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Emoji Picker" - ] - }, - "title": "Changing Emoji Picker Columns", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "suggestion-menus-emoji-picker-component", - "fullSlug": "ui-components/suggestion-menus-emoji-picker-component", - "pathFromRoot": "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + "Emoji Picker", + ], + }, + title: "Changing Emoji Picker Columns", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "suggestion-menus-emoji-picker-component", + fullSlug: "ui-components/suggestion-menus-emoji-picker-component", + pathFromRoot: + "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "UI Components", "Suggestion Menus", "Emoji Picker", - "Appearance & Styling" - ] - }, - "title": "Replacing Emoji Picker Component", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "suggestion-menus-grid-mentions", - "fullSlug": "ui-components/suggestion-menus-grid-mentions", - "pathFromRoot": "examples/03-ui-components/10-suggestion-menus-grid-mentions", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + "Appearance & Styling", + ], + }, + title: "Replacing Emoji Picker Component", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "suggestion-menus-grid-mentions", + fullSlug: "ui-components/suggestion-menus-grid-mentions", + pathFromRoot: + "examples/03-ui-components/10-suggestion-menus-grid-mentions", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus" - ] - }, - "title": "Grid Mentions Menu", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "uppy-file-panel", - "fullSlug": "ui-components/uppy-file-panel", - "pathFromRoot": "examples/03-ui-components/11-uppy-file-panel", - "config": { - "playground": true, - "docs": true, - "author": "ezhil56x", - "tags": [ - "Intermediate", - "Files" + "Suggestion Menus", ], - "dependencies": { + }, + title: "Grid Mentions Menu", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "uppy-file-panel", + fullSlug: "ui-components/uppy-file-panel", + pathFromRoot: "examples/03-ui-components/11-uppy-file-panel", + config: { + playground: true, + docs: true, + author: "ezhil56x", + tags: ["Intermediate", "Files"], + dependencies: { "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", "@uppy/drag-drop": "^3.1.1", @@ -592,46 +539,46 @@ "@uppy/status-bar": "^3.1.1", "@uppy/webcam": "^3.4.2", "@uppy/xhr-upload": "^3.4.0", - "react-icons": "^5.2.1" + "react-icons": "^5.2.1", } as any, - "pro": true - }, - "title": "Uppy File Panel", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "static-formatting-toolbar", - "fullSlug": "ui-components/static-formatting-toolbar", - "pathFromRoot": "examples/03-ui-components/12-static-formatting-toolbar", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + pro: true, + }, + title: "Uppy File Panel", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "static-formatting-toolbar", + fullSlug: "ui-components/static-formatting-toolbar", + pathFromRoot: "examples/03-ui-components/12-static-formatting-toolbar", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Basic", "UI Components", "Formatting Toolbar", - "Appearance & Styling" - ] - }, - "title": "Static Formatting Toolbar", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "custom-ui", - "fullSlug": "ui-components/custom-ui", - "pathFromRoot": "examples/03-ui-components/13-custom-ui", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + "Appearance & Styling", + ], + }, + title: "Static Formatting Toolbar", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "custom-ui", + fullSlug: "ui-components/custom-ui", + pathFromRoot: "examples/03-ui-components/13-custom-ui", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Advanced", "Inline Content", "UI Components", @@ -639,965 +586,884 @@ "Formatting Toolbar", "Suggestion Menus", "Slash Menu", - "Appearance & Styling" + "Appearance & Styling", ], - "dependencies": { + dependencies: { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.1", - "@mui/material": "^5.16.1" + "@mui/material": "^5.16.1", } as any, - "pro": true - }, - "title": "UI With Third-Party Components", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "experimental-mobile-formatting-toolbar", - "fullSlug": "ui-components/experimental-mobile-formatting-toolbar", - "pathFromRoot": "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", - "config": { - "playground": true, - "docs": true, - "author": "areknawo", - "tags": [ + pro: true, + }, + title: "UI With Third-Party Components", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "experimental-mobile-formatting-toolbar", + fullSlug: "ui-components/experimental-mobile-formatting-toolbar", + pathFromRoot: + "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", + config: { + playground: true, + docs: true, + author: "areknawo", + tags: [ "Intermediate", "UI Components", "Formatting Toolbar", - "Appearance & Styling" - ] - }, - "title": "Experimental Mobile Formatting Toolbar", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "advanced-tables", - "fullSlug": "ui-components/advanced-tables", - "pathFromRoot": "examples/03-ui-components/15-advanced-tables", - "config": { - "playground": true, - "docs": true, - "author": "nperez0111", - "tags": [ + "Appearance & Styling", + ], + }, + title: "Experimental Mobile Formatting Toolbar", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "advanced-tables", + fullSlug: "ui-components/advanced-tables", + pathFromRoot: "examples/03-ui-components/15-advanced-tables", + config: { + playground: true, + docs: true, + author: "nperez0111", + tags: [ "Intermediate", "UI Components", "Tables", - "Appearance & Styling" - ] - }, - "title": "Advanced Tables", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - }, - { - "projectSlug": "link-toolbar-buttons", - "fullSlug": "ui-components/link-toolbar-buttons", - "pathFromRoot": "examples/03-ui-components/link-toolbar-buttons", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Adding Link Toolbar Buttons", - "group": { - "pathFromRoot": "examples/03-ui-components", - "slug": "ui-components" - } - } - ] - }, - "theming": { - "pathFromRoot": "examples/04-theming", - "slug": "theming", - "projects": [ - { - "projectSlug": "theming-dom-attributes", - "fullSlug": "theming/theming-dom-attributes", - "pathFromRoot": "examples/04-theming/01-theming-dom-attributes", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Blocks", - "Appearance & Styling" - ] - }, - "title": "Adding CSS Class to Blocks", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "changing-font", - "fullSlug": "theming/changing-font", - "pathFromRoot": "examples/04-theming/02-changing-font", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Appearance & Styling" - ] - }, - "title": "Changing Editor Font", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "theming-css", - "fullSlug": "theming/theming-css", - "pathFromRoot": "examples/04-theming/03-theming-css", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Appearance & Styling", - "UI Components" - ] - }, - "title": "Overriding CSS Styles", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "theming-css-variables", - "fullSlug": "theming/theming-css-variables", - "pathFromRoot": "examples/04-theming/04-theming-css-variables", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Appearance & Styling", - "UI Components" - ] - }, - "title": "Overriding Theme CSS Variables", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "theming-css-variables-code", - "fullSlug": "theming/theming-css-variables-code", - "pathFromRoot": "examples/04-theming/05-theming-css-variables-code", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", "Appearance & Styling", - "UI Components" - ] - }, - "title": "Changing Themes Through Code", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "code-block", - "fullSlug": "theming/code-block", - "pathFromRoot": "examples/04-theming/06-code-block", - "config": { - "playground": true, - "docs": true, - "author": "nperez0111", - "tags": [ - "Basic" - ], - "dependencies": { - "@blocknote/code-block": "latest" - } as any - }, - "title": "Code Block Syntax Highlighting", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - }, - { - "projectSlug": "custom-code-block", - "fullSlug": "theming/custom-code-block", - "pathFromRoot": "examples/04-theming/07-custom-code-block", - "config": { - "playground": true, - "docs": true, - "author": "nperez0111", - "tags": [ - "Basic" ], - "dependencies": { + }, + title: "Advanced Tables", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + { + projectSlug: "link-toolbar-buttons", + fullSlug: "ui-components/link-toolbar-buttons", + pathFromRoot: "examples/03-ui-components/link-toolbar-buttons", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Adding Link Toolbar Buttons", + group: { + pathFromRoot: "examples/03-ui-components", + slug: "ui-components", + }, + }, + ], + }, + theming: { + pathFromRoot: "examples/04-theming", + slug: "theming", + projects: [ + { + projectSlug: "theming-dom-attributes", + fullSlug: "theming/theming-dom-attributes", + pathFromRoot: "examples/04-theming/01-theming-dom-attributes", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Blocks", "Appearance & Styling"], + }, + title: "Adding CSS Class to Blocks", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "changing-font", + fullSlug: "theming/changing-font", + pathFromRoot: "examples/04-theming/02-changing-font", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Appearance & Styling"], + }, + title: "Changing Editor Font", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "theming-css", + fullSlug: "theming/theming-css", + pathFromRoot: "examples/04-theming/03-theming-css", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Appearance & Styling", "UI Components"], + }, + title: "Overriding CSS Styles", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "theming-css-variables", + fullSlug: "theming/theming-css-variables", + pathFromRoot: "examples/04-theming/04-theming-css-variables", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Appearance & Styling", "UI Components"], + }, + title: "Overriding Theme CSS Variables", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "theming-css-variables-code", + fullSlug: "theming/theming-css-variables-code", + pathFromRoot: "examples/04-theming/05-theming-css-variables-code", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Appearance & Styling", "UI Components"], + }, + title: "Changing Themes Through Code", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "code-block", + fullSlug: "theming/code-block", + pathFromRoot: "examples/04-theming/06-code-block", + config: { + playground: true, + docs: true, + author: "nperez0111", + tags: ["Basic"], + dependencies: { + "@blocknote/code-block": "latest", + } as any, + }, + title: "Code Block Syntax Highlighting", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + { + projectSlug: "custom-code-block", + fullSlug: "theming/custom-code-block", + pathFromRoot: "examples/04-theming/07-custom-code-block", + config: { + playground: true, + docs: true, + author: "nperez0111", + tags: ["Basic"], + dependencies: { "@blocknote/code-block": "latest", "@shikijs/types": "^3.2.1", "@shikijs/core": "^3.2.1", "@shikijs/engine-javascript": "^3.2.1", "@shikijs/langs-precompiled": "^3.2.1", - "@shikijs/themes": "^3.2.1" - } as any - }, - "title": "Custom Code Block Theme & Language", - "group": { - "pathFromRoot": "examples/04-theming", - "slug": "theming" - } - } - ] + "@shikijs/themes": "^3.2.1", + } as any, + }, + title: "Custom Code Block Theme & Language", + group: { + pathFromRoot: "examples/04-theming", + slug: "theming", + }, + }, + ], }, - "interoperability": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability", - "projects": [ - { - "projectSlug": "converting-blocks-to-html", - "fullSlug": "interoperability/converting-blocks-to-html", - "pathFromRoot": "examples/05-interoperability/01-converting-blocks-to-html", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Blocks", - "Import/Export" - ] - }, - "title": "Converting Blocks to HTML", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-from-html", - "fullSlug": "interoperability/converting-blocks-from-html", - "pathFromRoot": "examples/05-interoperability/02-converting-blocks-from-html", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Basic", - "Blocks", - "Import/Export" - ] - }, - "title": "Parsing HTML to Blocks", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-to-md", - "fullSlug": "interoperability/converting-blocks-to-md", - "pathFromRoot": "examples/05-interoperability/03-converting-blocks-to-md", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic", - "Blocks", - "Import/Export" - ] - }, - "title": "Converting Blocks to Markdown", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-from-md", - "fullSlug": "interoperability/converting-blocks-from-md", - "pathFromRoot": "examples/05-interoperability/04-converting-blocks-from-md", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Basic", - "Blocks", - "Import/Export" - ] - }, - "title": "Parsing Markdown to Blocks", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-to-pdf", - "fullSlug": "interoperability/converting-blocks-to-pdf", - "pathFromRoot": "examples/05-interoperability/05-converting-blocks-to-pdf", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Interoperability" - ], - "dependencies": { + interoperability: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + projects: [ + { + projectSlug: "converting-blocks-to-html", + fullSlug: "interoperability/converting-blocks-to-html", + pathFromRoot: + "examples/05-interoperability/01-converting-blocks-to-html", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Blocks", "Import/Export"], + }, + title: "Converting Blocks to HTML", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-from-html", + fullSlug: "interoperability/converting-blocks-from-html", + pathFromRoot: + "examples/05-interoperability/02-converting-blocks-from-html", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Basic", "Blocks", "Import/Export"], + }, + title: "Parsing HTML to Blocks", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-to-md", + fullSlug: "interoperability/converting-blocks-to-md", + pathFromRoot: "examples/05-interoperability/03-converting-blocks-to-md", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic", "Blocks", "Import/Export"], + }, + title: "Converting Blocks to Markdown", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-from-md", + fullSlug: "interoperability/converting-blocks-from-md", + pathFromRoot: + "examples/05-interoperability/04-converting-blocks-from-md", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Basic", "Blocks", "Import/Export"], + }, + title: "Parsing Markdown to Blocks", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-to-pdf", + fullSlug: "interoperability/converting-blocks-to-pdf", + pathFromRoot: + "examples/05-interoperability/05-converting-blocks-to-pdf", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Interoperability"], + dependencies: { "@blocknote/xl-pdf-exporter": "latest", - "@react-pdf/renderer": "^4.3.0" + "@react-pdf/renderer": "^4.3.0", } as any, - "pro": true - }, - "title": "Exporting documents to PDF", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-to-docx", - "fullSlug": "interoperability/converting-blocks-to-docx", - "pathFromRoot": "examples/05-interoperability/06-converting-blocks-to-docx", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "" - ], - "dependencies": { + pro: true, + }, + title: "Exporting documents to PDF", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-to-docx", + fullSlug: "interoperability/converting-blocks-to-docx", + pathFromRoot: + "examples/05-interoperability/06-converting-blocks-to-docx", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [""], + dependencies: { "@blocknote/xl-docx-exporter": "latest", - "docx": "^9.0.2" + docx: "^9.0.2", } as any, - "pro": true - }, - "title": "Exporting documents to .docx (Office Open XML)", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-to-odt", - "fullSlug": "interoperability/converting-blocks-to-odt", - "pathFromRoot": "examples/05-interoperability/07-converting-blocks-to-odt", - "config": { - "playground": true, - "docs": true, - "author": "areknawo", - "tags": [ - "" - ], - "dependencies": { - "@blocknote/xl-odt-exporter": "latest" + pro: true, + }, + title: "Exporting documents to .docx (Office Open XML)", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-to-odt", + fullSlug: "interoperability/converting-blocks-to-odt", + pathFromRoot: + "examples/05-interoperability/07-converting-blocks-to-odt", + config: { + playground: true, + docs: true, + author: "areknawo", + tags: [""], + dependencies: { + "@blocknote/xl-odt-exporter": "latest", } as any, - "pro": true - }, - "title": "Exporting documents to .odt (Open Document Text)", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - }, - { - "projectSlug": "converting-blocks-to-react-email", - "fullSlug": "interoperability/converting-blocks-to-react-email", - "pathFromRoot": "examples/05-interoperability/08-converting-blocks-to-react-email", - "config": { - "playground": true, - "docs": true, - "author": "jmarbutt", - "tags": [ - "" - ], - "dependencies": { - "@blocknote/xl-react-email-exporter": "latest", - "@react-email/render": "^1.1.2" + pro: true, + }, + title: "Exporting documents to .odt (Open Document Text)", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + { + projectSlug: "converting-blocks-to-react-email", + fullSlug: "interoperability/converting-blocks-to-react-email", + pathFromRoot: + "examples/05-interoperability/08-converting-blocks-to-react-email", + config: { + playground: true, + docs: true, + author: "jmarbutt", + tags: [""], + dependencies: { + "@blocknote/xl-email-exporter": "latest", + "@react-email/render": "^1.1.2", } as any, - "pro": true - }, - "title": "Exporting documents to React Email", - "group": { - "pathFromRoot": "examples/05-interoperability", - "slug": "interoperability" - } - } - ] + pro: true, + }, + title: "Exporting documents to React Email", + group: { + pathFromRoot: "examples/05-interoperability", + slug: "interoperability", + }, + }, + ], }, "custom-schema": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema", - "projects": [ - { - "projectSlug": "alert-block", - "fullSlug": "custom-schema/alert-block", - "pathFromRoot": "examples/06-custom-schema/01-alert-block", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + projects: [ + { + projectSlug: "alert-block", + fullSlug: "custom-schema/alert-block", + pathFromRoot: "examples/06-custom-schema/01-alert-block", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu" + "Slash Menu", ], - "dependencies": { + dependencies: { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1" - } as any - }, - "title": "Alert Block", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "suggestion-menus-mentions", - "fullSlug": "custom-schema/suggestion-menus-mentions", - "pathFromRoot": "examples/06-custom-schema/02-suggestion-menus-mentions", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ + "react-icons": "^5.2.1", + } as any, + }, + title: "Alert Block", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "suggestion-menus-mentions", + fullSlug: "custom-schema/suggestion-menus-mentions", + pathFromRoot: "examples/06-custom-schema/02-suggestion-menus-mentions", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus" - ] - }, - "title": "Mentions Menu", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "font-style", - "fullSlug": "custom-schema/font-style", - "pathFromRoot": "examples/06-custom-schema/03-font-style", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + "Suggestion Menus", + ], + }, + title: "Mentions Menu", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "font-style", + fullSlug: "custom-schema/font-style", + pathFromRoot: "examples/06-custom-schema/03-font-style", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Inline Content", "Custom Schemas", - "Formatting Toolbar" + "Formatting Toolbar", ], - "dependencies": { - "react-icons": "^5.2.1" - } as any - }, - "title": "Font Style", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "pdf-file-block", - "fullSlug": "custom-schema/pdf-file-block", - "pathFromRoot": "examples/06-custom-schema/04-pdf-file-block", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + dependencies: { + "react-icons": "^5.2.1", + } as any, + }, + title: "Font Style", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "pdf-file-block", + fullSlug: "custom-schema/pdf-file-block", + pathFromRoot: "examples/06-custom-schema/04-pdf-file-block", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu" + "Slash Menu", ], - "dependencies": { + dependencies: { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1" + "react-icons": "^5.2.1", } as any, - "pro": true - }, - "title": "PDF Block", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "alert-block-full-ux", - "fullSlug": "custom-schema/alert-block-full-ux", - "pathFromRoot": "examples/06-custom-schema/05-alert-block-full-ux", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ + pro: true, + }, + title: "PDF Block", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "alert-block-full-ux", + fullSlug: "custom-schema/alert-block-full-ux", + pathFromRoot: "examples/06-custom-schema/05-alert-block-full-ux", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: [ "Intermediate", "Blocks", "Custom Schemas", "Formatting Toolbar", "Suggestion Menus", - "Slash Menu" + "Slash Menu", ], - "dependencies": { + dependencies: { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1" - } as any - }, - "title": "Alert Block with Full UX", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "react-custom-blocks", - "fullSlug": "custom-schema/react-custom-blocks", - "pathFromRoot": "examples/06-custom-schema/react-custom-blocks", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Blocks - React API", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "react-custom-inline-content", - "fullSlug": "custom-schema/react-custom-inline-content", - "pathFromRoot": "examples/06-custom-schema/react-custom-inline-content", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Inline Content - React API", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - }, - { - "projectSlug": "react-custom-styles", - "fullSlug": "custom-schema/react-custom-styles", - "pathFromRoot": "examples/06-custom-schema/react-custom-styles", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Styles - React API", - "group": { - "pathFromRoot": "examples/06-custom-schema", - "slug": "custom-schema" - } - } - ] + "react-icons": "^5.2.1", + } as any, + }, + title: "Alert Block with Full UX", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "react-custom-blocks", + fullSlug: "custom-schema/react-custom-blocks", + pathFromRoot: "examples/06-custom-schema/react-custom-blocks", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Blocks - React API", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "react-custom-inline-content", + fullSlug: "custom-schema/react-custom-inline-content", + pathFromRoot: "examples/06-custom-schema/react-custom-inline-content", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Inline Content - React API", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + { + projectSlug: "react-custom-styles", + fullSlug: "custom-schema/react-custom-styles", + pathFromRoot: "examples/06-custom-schema/react-custom-styles", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Styles - React API", + group: { + pathFromRoot: "examples/06-custom-schema", + slug: "custom-schema", + }, + }, + ], }, - "collaboration": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration", - "projects": [ - { - "projectSlug": "partykit", - "fullSlug": "collaboration/partykit", - "pathFromRoot": "examples/07-collaboration/01-partykit", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Advanced", - "Saving/Loading", - "Collaboration" - ], - "dependencies": { + collaboration: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + projects: [ + { + projectSlug: "partykit", + fullSlug: "collaboration/partykit", + pathFromRoot: "examples/07-collaboration/01-partykit", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Advanced", "Saving/Loading", "Collaboration"], + dependencies: { "y-partykit": "^0.0.25", - "yjs": "^13.6.15" - } as any - }, - "title": "Collaborative Editing with PartyKit", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "liveblocks", - "fullSlug": "collaboration/liveblocks", - "pathFromRoot": "examples/07-collaboration/02-liveblocks", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Advanced", - "Saving/Loading", - "Collaboration" - ], - "dependencies": { + yjs: "^13.6.15", + } as any, + }, + title: "Collaborative Editing with PartyKit", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "liveblocks", + fullSlug: "collaboration/liveblocks", + pathFromRoot: "examples/07-collaboration/02-liveblocks", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Advanced", "Saving/Loading", "Collaboration"], + dependencies: { "@liveblocks/client": "^2.23.1", "@liveblocks/react": "^2.23.1", "@liveblocks/react-blocknote": "^2.23.1", "@liveblocks/react-tiptap": "^2.23.1", "@liveblocks/react-ui": "^2.23.1", - "yjs": "^13.6.15" - } as any - }, - "title": "Collaborative Editing with Liveblocks", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "y-sweet", - "fullSlug": "collaboration/y-sweet", - "pathFromRoot": "examples/07-collaboration/03-y-sweet", - "config": { - "playground": true, - "docs": true, - "author": "jakelazaroff", - "tags": [ - "Advanced", - "Saving/Loading", - "Collaboration" - ], - "dependencies": { - "@y-sweet/react": "^0.6.3" - } as any - }, - "title": "Collaborative Editing with Y-Sweet", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "comments", - "fullSlug": "collaboration/comments", - "pathFromRoot": "examples/07-collaboration/04-comments", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "Advanced", - "Comments", - "Collaboration" - ], - "dependencies": { + yjs: "^13.6.15", + } as any, + }, + title: "Collaborative Editing with Liveblocks", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "y-sweet", + fullSlug: "collaboration/y-sweet", + pathFromRoot: "examples/07-collaboration/03-y-sweet", + config: { + playground: true, + docs: true, + author: "jakelazaroff", + tags: ["Advanced", "Saving/Loading", "Collaboration"], + dependencies: { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^7.10.1" - } as any - }, - "title": "Comments & Threads", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "comments-with-sidebar", - "fullSlug": "collaboration/comments-with-sidebar", - "pathFromRoot": "examples/07-collaboration/05-comments-with-sidebar", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "Advanced", - "Comments", - "Collaboration" - ], - "dependencies": { + } as any, + }, + title: "Collaborative Editing with Y-Sweet", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "comments", + fullSlug: "collaboration/comments", + pathFromRoot: "examples/07-collaboration/04-comments", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["Advanced", "Comments", "Collaboration"], + dependencies: { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^7.10.1" - } as any - }, - "title": "Threads Sidebar", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "ghost-writer", - "fullSlug": "collaboration/ghost-writer", - "pathFromRoot": "examples/07-collaboration/06-ghost-writer", - "config": { - "playground": true, - "docs": false, - "author": "nperez0111", - "tags": [ - "Advanced", - "Development", - "Collaboration" - ], - "dependencies": { + "@mantine/core": "^7.10.1", + } as any, + }, + title: "Comments & Threads", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "comments-with-sidebar", + fullSlug: "collaboration/comments-with-sidebar", + pathFromRoot: "examples/07-collaboration/05-comments-with-sidebar", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["Advanced", "Comments", "Collaboration"], + dependencies: { + "@y-sweet/react": "^0.6.3", + "@mantine/core": "^7.10.1", + } as any, + }, + title: "Threads Sidebar", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "ghost-writer", + fullSlug: "collaboration/ghost-writer", + pathFromRoot: "examples/07-collaboration/06-ghost-writer", + config: { + playground: true, + docs: false, + author: "nperez0111", + tags: ["Advanced", "Development", "Collaboration"], + dependencies: { "y-partykit": "^0.0.25", - "yjs": "^13.6.15" - } as any - }, - "title": "Ghost Writer", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - }, - { - "projectSlug": "forking", - "fullSlug": "collaboration/forking", - "pathFromRoot": "examples/07-collaboration/07-forking", - "config": { - "playground": true, - "docs": false, - "author": "nperez0111", - "tags": [ - "Advanced", - "Development", - "Collaboration" - ], - "dependencies": { + yjs: "^13.6.15", + } as any, + }, + title: "Ghost Writer", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + { + projectSlug: "forking", + fullSlug: "collaboration/forking", + pathFromRoot: "examples/07-collaboration/07-forking", + config: { + playground: true, + docs: false, + author: "nperez0111", + tags: ["Advanced", "Development", "Collaboration"], + dependencies: { "y-partykit": "^0.0.25", - "yjs": "^13.6.15" - } as any - }, - "title": "Collaborative Editing with Forking", - "group": { - "pathFromRoot": "examples/07-collaboration", - "slug": "collaboration" - } - } - ] + yjs: "^13.6.15", + } as any, + }, + title: "Collaborative Editing with Forking", + group: { + pathFromRoot: "examples/07-collaboration", + slug: "collaboration", + }, + }, + ], }, - "extensions": { - "pathFromRoot": "examples/08-extensions", - "slug": "extensions", - "projects": [ - { - "projectSlug": "tiptap-arrow-conversion", - "fullSlug": "extensions/tiptap-arrow-conversion", - "pathFromRoot": "examples/08-extensions/01-tiptap-arrow-conversion", - "config": { - "playground": true, - "docs": true, - "author": "komsenapati", - "tags": [ - "Extension" - ], - "pro": true, - "dependencies": { - "@tiptap/core": "^2.12.0" - } as any - }, - "title": "TipTap extension (arrow InputRule)", - "group": { - "pathFromRoot": "examples/08-extensions", - "slug": "extensions" - } - } - ] + extensions: { + pathFromRoot: "examples/08-extensions", + slug: "extensions", + projects: [ + { + projectSlug: "tiptap-arrow-conversion", + fullSlug: "extensions/tiptap-arrow-conversion", + pathFromRoot: "examples/08-extensions/01-tiptap-arrow-conversion", + config: { + playground: true, + docs: true, + author: "komsenapati", + tags: ["Extension"], + pro: true, + dependencies: { + "@tiptap/core": "^2.12.0", + } as any, + }, + title: "TipTap extension (arrow InputRule)", + group: { + pathFromRoot: "examples/08-extensions", + slug: "extensions", + }, + }, + ], }, - "ai": { - "pathFromRoot": "examples/09-ai", - "slug": "ai", - "projects": [ - { - "projectSlug": "minimal", - "fullSlug": "ai/minimal", - "pathFromRoot": "examples/09-ai/01-minimal", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "AI", - "llm" - ], - "dependencies": { + ai: { + pathFromRoot: "examples/09-ai", + slug: "ai", + projects: [ + { + projectSlug: "minimal", + fullSlug: "ai/minimal", + pathFromRoot: "examples/09-ai/01-minimal", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["AI", "llm"], + dependencies: { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - "ai": "^4.3.15", + ai: "^4.3.15", "@ai-sdk/groq": "^1.2.9", - "zustand": "^5.0.3" - } as any - }, - "title": "Rich Text editor AI integration", - "group": { - "pathFromRoot": "examples/09-ai", - "slug": "ai" - } - }, - { - "projectSlug": "playground", - "fullSlug": "ai/playground", - "pathFromRoot": "examples/09-ai/02-playground", - "config": { - "playground": true, - "docs": true, - "author": "yousefed", - "tags": [ - "AI", - "llm" - ], - "dependencies": { + zustand: "^5.0.3", + } as any, + }, + title: "Rich Text editor AI integration", + group: { + pathFromRoot: "examples/09-ai", + slug: "ai", + }, + }, + { + projectSlug: "playground", + fullSlug: "ai/playground", + pathFromRoot: "examples/09-ai/02-playground", + config: { + playground: true, + docs: true, + author: "yousefed", + tags: ["AI", "llm"], + dependencies: { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - "ai": "^4.3.15", + ai: "^4.3.15", "@ai-sdk/openai": "^1.3.22", "@ai-sdk/openai-compatible": "^0.2.14", "@ai-sdk/groq": "^1.2.9", "@ai-sdk/anthropic": "^1.2.11", "@ai-sdk/mistral": "^1.2.8", - "zustand": "^5.0.3" - } as any - }, - "title": "AI Playground", - "group": { - "pathFromRoot": "examples/09-ai", - "slug": "ai" - } - }, - { - "projectSlug": "custom-ai-menu-items", - "fullSlug": "ai/custom-ai-menu-items", - "pathFromRoot": "examples/09-ai/03-custom-ai-menu-items", - "config": { - "playground": true, - "docs": true, - "author": "matthewlipski", - "tags": [ - "AI", - "llm" - ], - "dependencies": { + zustand: "^5.0.3", + } as any, + }, + title: "AI Playground", + group: { + pathFromRoot: "examples/09-ai", + slug: "ai", + }, + }, + { + projectSlug: "custom-ai-menu-items", + fullSlug: "ai/custom-ai-menu-items", + pathFromRoot: "examples/09-ai/03-custom-ai-menu-items", + config: { + playground: true, + docs: true, + author: "matthewlipski", + tags: ["AI", "llm"], + dependencies: { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - "ai": "^4.1.0", + ai: "^4.1.0", "@ai-sdk/openai": "^1.1.0", "@ai-sdk/groq": "^1.1.0", "react-icons": "^5.2.1", - "zustand": "^5.0.3" - } as any - }, - "title": "Adding AI Menu Items", - "group": { - "pathFromRoot": "examples/09-ai", - "slug": "ai" - } - }, - { - "projectSlug": "with-collaboration", - "fullSlug": "ai/with-collaboration", - "pathFromRoot": "examples/09-ai/04-with-collaboration", - "config": { - "playground": true, - "docs": false, - "author": "nperez0111", - "tags": [ - "AI", - "llm" - ], - "dependencies": { + zustand: "^5.0.3", + } as any, + }, + title: "Adding AI Menu Items", + group: { + pathFromRoot: "examples/09-ai", + slug: "ai", + }, + }, + { + projectSlug: "with-collaboration", + fullSlug: "ai/with-collaboration", + pathFromRoot: "examples/09-ai/04-with-collaboration", + config: { + playground: true, + docs: false, + author: "nperez0111", + tags: ["AI", "llm"], + dependencies: { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - "ai": "^4.3.15", + ai: "^4.3.15", "@ai-sdk/groq": "^1.2.9", "y-partykit": "^0.0.25", - "yjs": "^13.6.15", - "zustand": "^5.0.3" - } as any - }, - "title": "AI + Ghost Writer", - "group": { - "pathFromRoot": "examples/09-ai", - "slug": "ai" - } - } - ] + yjs: "^13.6.15", + zustand: "^5.0.3", + } as any, + }, + title: "AI + Ghost Writer", + group: { + pathFromRoot: "examples/09-ai", + slug: "ai", + }, + }, + ], }, "vanilla-js": { - "pathFromRoot": "examples/vanilla-js", - "slug": "vanilla-js", - "projects": [ - { - "projectSlug": "react-vanilla-custom-blocks", - "fullSlug": "vanilla-js/react-vanilla-custom-blocks", - "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-blocks", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Blocks - Vanilla JS API", - "group": { - "pathFromRoot": "examples/vanilla-js", - "slug": "vanilla-js" - } - }, - { - "projectSlug": "react-vanilla-custom-inline-content", - "fullSlug": "vanilla-js/react-vanilla-custom-inline-content", - "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-inline-content", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Inline Content - Vanilla JS API", - "group": { - "pathFromRoot": "examples/vanilla-js", - "slug": "vanilla-js" - } - }, - { - "projectSlug": "react-vanilla-custom-styles", - "fullSlug": "vanilla-js/react-vanilla-custom-styles", - "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-styles", - "config": { - "playground": true, - "docs": false, - "author": "matthewlipski", - "tags": [] - }, - "title": "Custom Styles - Vanilla JS API", - "group": { - "pathFromRoot": "examples/vanilla-js", - "slug": "vanilla-js" - } - } - ] - } -}; \ No newline at end of file + pathFromRoot: "examples/vanilla-js", + slug: "vanilla-js", + projects: [ + { + projectSlug: "react-vanilla-custom-blocks", + fullSlug: "vanilla-js/react-vanilla-custom-blocks", + pathFromRoot: "examples/vanilla-js/react-vanilla-custom-blocks", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Blocks - Vanilla JS API", + group: { + pathFromRoot: "examples/vanilla-js", + slug: "vanilla-js", + }, + }, + { + projectSlug: "react-vanilla-custom-inline-content", + fullSlug: "vanilla-js/react-vanilla-custom-inline-content", + pathFromRoot: "examples/vanilla-js/react-vanilla-custom-inline-content", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Inline Content - Vanilla JS API", + group: { + pathFromRoot: "examples/vanilla-js", + slug: "vanilla-js", + }, + }, + { + projectSlug: "react-vanilla-custom-styles", + fullSlug: "vanilla-js/react-vanilla-custom-styles", + pathFromRoot: "examples/vanilla-js/react-vanilla-custom-styles", + config: { + playground: true, + docs: false, + author: "matthewlipski", + tags: [], + }, + title: "Custom Styles - Vanilla JS API", + group: { + pathFromRoot: "examples/vanilla-js", + slug: "vanilla-js", + }, + }, + ], + }, +}; diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 3c4bfdae03..10455b2d4c 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -60,7 +60,7 @@ export default defineConfig((conf) => ({ __dirname, "../../liveblocks/packages/liveblocks-react-blocknote/src/", ), - "@blocknote/xl-react-email-exporter": resolve( + "@blocknote/xl-email-exporter": resolve( __dirname, "../packages/xl-react-email-exporter/src", ), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 866fb1d3dc..12b94fbad9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2121,9 +2121,9 @@ importers: '@blocknote/shadcn': specifier: latest version: link:../../../packages/shadcn - '@blocknote/xl-react-email-exporter': + '@blocknote/xl-email-exporter': specifier: latest - version: link:../../../packages/xl-react-email-exporter + version: link:../../../packages/xl-email-exporter '@react-email/render': specifier: ^1.1.2 version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3996,6 +3996,61 @@ importers: specifier: ^3.6.3 version: 3.6.5 + packages/xl-email-exporter: + dependencies: + '@blocknote/core': + specifier: 0.31.3 + version: link:../core + '@blocknote/react': + specifier: 0.31.3 + version: link:../react + '@react-email/components': + specifier: ^0.1.0 + version: 0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/render': + specifier: ^1.1.2 + version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + react: + specifier: ^18 + version: 18.3.1 + react-dom: + specifier: ^18 + version: 18.3.1(react@18.3.1) + react-email: + specifier: ^4.0.16 + version: 4.0.16(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 + '@types/react': + specifier: ^18.0.25 + version: 18.3.20 + '@types/react-dom': + specifier: ^18.0.9 + version: 18.3.5(@types/react@18.3.20) + eslint: + specifier: ^8.10.0 + version: 8.57.1 + rollup-plugin-webpack-stats: + specifier: ^0.2.2 + version: 0.2.6(rollup@4.37.0) + typescript: + specifier: ^5.0.4 + version: 5.8.2 + vite: + specifier: ^5.3.4 + version: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) + vite-plugin-eslint: + specifier: ^1.8.1 + version: 1.8.1(eslint@8.57.1)(vite@5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2)) + vitest: + specifier: ^2.0.3 + version: 2.1.9(@types/node@22.14.1)(@vitest/ui@2.1.9)(jsdom@25.0.1(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(msw@2.7.3(@types/node@22.14.1)(typescript@5.8.2))(terser@5.39.2) + packages/xl-multi-column: dependencies: '@blocknote/core': @@ -4182,61 +4237,6 @@ importers: specifier: ^2.0.3 version: 2.1.9(@types/node@22.14.1)(@vitest/ui@2.1.9)(jsdom@25.0.1(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(msw@2.7.3(@types/node@22.14.1)(typescript@5.8.2))(terser@5.39.2) - packages/xl-react-email-exporter: - dependencies: - '@blocknote/core': - specifier: 0.31.3 - version: link:../core - '@blocknote/react': - specifier: 0.31.3 - version: link:../react - '@react-email/components': - specifier: ^0.1.0 - version: 0.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-email/render': - specifier: ^1.1.2 - version: 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - buffer: - specifier: ^6.0.3 - version: 6.0.3 - react: - specifier: ^18 - version: 18.3.1 - react-dom: - specifier: ^18 - version: 18.3.1(react@18.3.1) - react-email: - specifier: ^4.0.16 - version: 4.0.16(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - devDependencies: - '@types/jsdom': - specifier: ^21.1.7 - version: 21.1.7 - '@types/react': - specifier: ^18.0.25 - version: 18.3.20 - '@types/react-dom': - specifier: ^18.0.9 - version: 18.3.5(@types/react@18.3.20) - eslint: - specifier: ^8.10.0 - version: 8.57.1 - rollup-plugin-webpack-stats: - specifier: ^0.2.2 - version: 0.2.6(rollup@4.37.0) - typescript: - specifier: ^5.0.4 - version: 5.8.2 - vite: - specifier: ^5.3.4 - version: 5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2) - vite-plugin-eslint: - specifier: ^1.8.1 - version: 1.8.1(eslint@8.57.1)(vite@5.4.15(@types/node@22.14.1)(lightningcss@1.30.1)(terser@5.39.2)) - vitest: - specifier: ^2.0.3 - version: 2.1.9(@types/node@22.14.1)(@vitest/ui@2.1.9)(jsdom@25.0.1(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.1)(msw@2.7.3(@types/node@22.14.1)(typescript@5.8.2))(terser@5.39.2) - playground: dependencies: '@ai-sdk/anthropic': @@ -4287,6 +4287,9 @@ importers: '@blocknote/xl-docx-exporter': specifier: workspace:^ version: link:../packages/xl-docx-exporter + '@blocknote/xl-email-exporter': + specifier: workspace:^ + version: link:../packages/xl-email-exporter '@blocknote/xl-multi-column': specifier: workspace:^ version: link:../packages/xl-multi-column @@ -4296,9 +4299,6 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:^ version: link:../packages/xl-pdf-exporter - '@blocknote/xl-react-email-exporter': - specifier: workspace:^ - version: link:../packages/xl-react-email-exporter '@emotion/react': specifier: ^11.11.4 version: 11.14.0(@types/react@18.3.20)(react@18.3.1) @@ -16549,12 +16549,12 @@ snapshots: '@babel/traverse@7.27.0': dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.0 '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -19861,7 +19861,7 @@ snapshots: '@types/cors@2.8.17': dependencies: - '@types/node': 20.17.45 + '@types/node': 20.17.50 '@types/d3-scale-chromatic@3.1.0': {} @@ -21849,7 +21849,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.17 - '@types/node': 20.17.45 + '@types/node': 20.17.50 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -22114,7 +22114,7 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) @@ -22161,7 +22161,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -22176,14 +22176,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -22206,7 +22206,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 991c00a1b9..7f6d7604f9 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -6,7 +6,7 @@ export default defineWorkspace([ "./packages/shadcn/vite.config.ts", "./packages/server-util/vite.config.ts", "./packages/xl-pdf-exporter/vite.config.ts", - "./packages/xl-react-email-exporter/vite.config.ts", + "./packages/xl-email-exporter/vite.config.ts", "./packages/xl-ai/vite.config.ts", "./packages/mantine/vite.config.ts", "./packages/core/vite.config.ts", From 3019305c4472827a57b8f1a79221236075adf606 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 12:16:08 +0200 Subject: [PATCH 35/38] docs: add docs for email export --- docs/pages/docs/editor-api/_meta.json | 1 + .../pages/docs/editor-api/export-to-email.mdx | 108 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docs/pages/docs/editor-api/export-to-email.mdx diff --git a/docs/pages/docs/editor-api/_meta.json b/docs/pages/docs/editor-api/_meta.json index 4f2961a73b..af00c8deec 100644 --- a/docs/pages/docs/editor-api/_meta.json +++ b/docs/pages/docs/editor-api/_meta.json @@ -7,6 +7,7 @@ "export-to-pdf": "", "export-to-docx": "", "export-to-odt": "", + "export-to-email": "", "events": "", "methods": "" } diff --git a/docs/pages/docs/editor-api/export-to-email.mdx b/docs/pages/docs/editor-api/export-to-email.mdx new file mode 100644 index 0000000000..3786bdd334 --- /dev/null +++ b/docs/pages/docs/editor-api/export-to-email.mdx @@ -0,0 +1,108 @@ +--- +title: Export to email +description: Export BlockNote documents to an email. +imageTitle: Export to email +path: /docs/export-to-email +--- + +import { Example } from "@/components/example"; +import { Callout } from "nextra/components"; + +# Exporting blocks to email + +Leveraging the [React Email](https://react.email/) library, it's possible to export BlockNote documents to email, completely client-side. + + + This feature is provided by the `@blocknote/xl-email-exporter`. `xl-` packages + are fully open source, but released under a copyleft license. A commercial + license for usage in closed source, proprietary products comes as part of the + [Business subscription](/pricing). + + +First, install the `@blocknote/xl-email-exporter` packages: + +```bash +npm install @blocknote/xl-email-exporter +``` + +Then, create an instance of the `ReactEmailExporter` class. This exposes the following methods: + +```typescript +import { + ReactEmailExporter, + reactEmailDefaultSchemaMappings, +} from "@blocknote/xl-email-exporter"; + +// Create the exporter +const exporter = new ReactEmailExporter(editor.schema, reactEmailDefaultSchemaMappings); + +// Convert the blocks to a react-email document +const html = await exporter.toReactEmailDocument(editor.document); + +// Use react-email to write to file: +await ReactEmail.render(html, `filename.html`); +``` + +See the [full example](/examples/interoperability/converting-blocks-to-react-email) with live React Email preview below: + + + +### Customizing the Email + +`toReactEmailDocument` takes an optional `options` parameter, which allows you to customize: + + - **preview**: Set the preview text for the email (can be a string or an array of strings) + - **header**: Add content to the top of the email (must be a React-Email compatible component) + - **footer**: Add content to the bottom of the email (must be a React-Email compatible component) + - **head**: Inject elements into the [Head element](https://react.email/docs/components/head) + +Example usage: + +```tsx +import { Text } from "@react-email/components"; +const html = await exporter.toReactEmailDocument(editor.document, { + preview: "This is a preview of the email content", + header: Header, + footer: Footer, + head: My email, +}); +``` + +### Custom mappings / custom schemas + +The `ReactEmailExporter` constructor takes a `schema` and `mappings` parameter. +A _mapping_ defines how to convert a BlockNote schema element (a Block, Inline Content, or Style) to a React-Email element. +If you're using a [custom schema](/docs/custom-schemas) in your editor, or if you want to overwrite how default BlockNote elements are converted to React Email, you can pass your own `mappings`: + +For example, use the following code in case your schema has an `extraBlock` type: + +```typescript +import { ReactEmailExporter, reactEmailDefaultSchemaMappings } from "@blocknote/xl-email-exporter"; +import { Text } from "@react-email/components"; + +new ReactEmailExporter(schema, { + blockMapping: { + ...reactEmailDefaultSchemaMappings.blockMapping, + myCustomBlock: (block, exporter) => { + return My custom block; + }, + }, + inlineContentMapping: reactEmailDefaultSchemaMappings.inlineContentMapping, + styleMapping: reactEmailDefaultSchemaMappings.styleMapping, +}); +``` + +### Exporter options + +The `ReactEmailExporter` constructor takes an optional `options` parameter. +While conversion happens on the client-side, the default setup uses two server based resources: + +```typescript +const defaultOptions = { + // a function to resolve external resources in order to avoid CORS issues + // by default, this calls a BlockNote hosted server-side proxy to resolve files + resolveFileUrl: corsProxyResolveFileUrl, + // the colors to use in the email + colors: COLORS_DEFAULT, // defaults from @blocknote/core +}; +``` From 9fcfd49785cefa34f511f7848411e775cf30f8bd Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 12:29:29 +0200 Subject: [PATCH 36/38] chore: rename other tsconfig files --- packages/xl-email-exporter/tsconfig.json | 79 +- packages/xl-email-exporter/vite.config.ts | 4 +- playground/src/examples.gen.tsx | 2776 +++++++++++---------- playground/tsconfig.json | 3 +- playground/vite.config.ts | 2 +- 5 files changed, 1496 insertions(+), 1368 deletions(-) diff --git a/packages/xl-email-exporter/tsconfig.json b/packages/xl-email-exporter/tsconfig.json index d8f00bf6a3..dcf6b07cb8 100644 --- a/packages/xl-email-exporter/tsconfig.json +++ b/packages/xl-email-exporter/tsconfig.json @@ -1,45 +1,38 @@ { - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": [ - "ESNext", - "DOM" - ], - "moduleResolution": "Node", - "jsx": "react-jsx", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "noEmit": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "outDir": "dist", - "declaration": true, - "declarationDir": "types", - "composite": true, - "skipLibCheck": true, - "paths": { - "@shared/*": [ - "../../shared/*" - ] - } + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true, + "paths": { + "@shared/*": ["../../shared/*"] + } + }, + "include": ["src"], + "references": [ + { + "path": "../core" }, - "include": [ - "src" - ], - "references": [ - { - "path": "../core" - }, - { - "path": "../react" - }, - { - "path": "../../shared" - } - ] -} \ No newline at end of file + { + "path": "../react" + }, + { + "path": "../../shared" + } + ] +} diff --git a/packages/xl-email-exporter/vite.config.ts b/packages/xl-email-exporter/vite.config.ts index 9d5ea9f7d8..535e4c78fc 100644 --- a/packages/xl-email-exporter/vite.config.ts +++ b/packages/xl-email-exporter/vite.config.ts @@ -42,8 +42,8 @@ export default defineConfig((conf) => ({ sourcemap: true, lib: { entry: path.resolve(__dirname, "src/index.ts"), - name: "blocknote-xl-react-email-exporter", - fileName: "blocknote-xl-react-email-exporter", + name: "blocknote-xl-email-exporter", + fileName: "blocknote-xl-email-exporter", }, rollupOptions: { // make sure to externalize deps that shouldn't be bundled diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 39b3880f4b..9a12ee22fd 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1,533 +1,586 @@ // generated by dev-scripts/examples/gen.ts -export const examples = { - basic: { - pathFromRoot: "examples/01-basic", - slug: "basic", - projects: [ - { - projectSlug: "minimal", - fullSlug: "basic/minimal", - pathFromRoot: "examples/01-basic/01-minimal", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Basic Setup", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "block-objects", - fullSlug: "basic/block-objects", - pathFromRoot: "examples/01-basic/02-block-objects", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Inline Content"], - }, - title: "Displaying Document JSON", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "multi-column", - fullSlug: "basic/multi-column", - pathFromRoot: "examples/01-basic/03-multi-column", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks"], - dependencies: { - "@blocknote/xl-multi-column": "latest", + export const examples = { + "basic": { + "pathFromRoot": "examples/01-basic", + "slug": "basic", + "projects": [ + { + "projectSlug": "minimal", + "fullSlug": "basic/minimal", + "pathFromRoot": "examples/01-basic/01-minimal", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Basic Setup", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "block-objects", + "fullSlug": "basic/block-objects", + "pathFromRoot": "examples/01-basic/02-block-objects", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Inline Content" + ] + }, + "title": "Displaying Document JSON", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "multi-column", + "fullSlug": "basic/multi-column", + "pathFromRoot": "examples/01-basic/03-multi-column", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks" + ], + "dependencies": { + "@blocknote/xl-multi-column": "latest" } as any, - pro: true, - }, - title: "Multi-Column Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "default-blocks", - fullSlug: "basic/default-blocks", - pathFromRoot: "examples/01-basic/04-default-blocks", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Inline Content"], - }, - title: "Default Schema Showcase", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "removing-default-blocks", - fullSlug: "basic/removing-default-blocks", - pathFromRoot: "examples/01-basic/05-removing-default-blocks", - config: { - playground: true, - docs: true, - author: "hunxjunedo", - tags: ["Basic", "removing", "blocks"], - }, - title: "Removing Default Blocks from Schema", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "block-manipulation", - fullSlug: "basic/block-manipulation", - pathFromRoot: "examples/01-basic/06-block-manipulation", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks"], - }, - title: "Manipulating Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "selection-blocks", - fullSlug: "basic/selection-blocks", - pathFromRoot: "examples/01-basic/07-selection-blocks", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks"], - }, - title: "Displaying Selected Blocks", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "ariakit", - fullSlug: "basic/ariakit", - pathFromRoot: "examples/01-basic/08-ariakit", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Use with Ariakit", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "shadcn", - fullSlug: "basic/shadcn", - pathFromRoot: "examples/01-basic/09-shadcn", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic"], - }, - title: "Use with ShadCN", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "localization", - fullSlug: "basic/localization", - pathFromRoot: "examples/01-basic/10-localization", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic"], - }, - title: "Localization (i18n)", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "custom-placeholder", - fullSlug: "basic/custom-placeholder", - pathFromRoot: "examples/01-basic/11-custom-placeholder", - config: { - playground: true, - docs: true, - author: "ezhil56x", - tags: ["Basic"], - }, - title: "Change placeholder text", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "multi-editor", - fullSlug: "basic/multi-editor", - pathFromRoot: "examples/01-basic/12-multi-editor", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: ["Basic"], - }, - title: "Multi-Editor Setup", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "custom-paste-handler", - fullSlug: "basic/custom-paste-handler", - pathFromRoot: "examples/01-basic/13-custom-paste-handler", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - }, - title: "Custom Paste Handler", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - { - projectSlug: "testing", - fullSlug: "basic/testing", - pathFromRoot: "examples/01-basic/testing", - config: { - playground: true, - docs: false, - }, - title: "Test Editor", - group: { - pathFromRoot: "examples/01-basic", - slug: "basic", - }, - }, - ], + "pro": true + }, + "title": "Multi-Column Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "default-blocks", + "fullSlug": "basic/default-blocks", + "pathFromRoot": "examples/01-basic/04-default-blocks", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Inline Content" + ] + }, + "title": "Default Schema Showcase", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "removing-default-blocks", + "fullSlug": "basic/removing-default-blocks", + "pathFromRoot": "examples/01-basic/05-removing-default-blocks", + "config": { + "playground": true, + "docs": true, + "author": "hunxjunedo", + "tags": [ + "Basic", + "removing", + "blocks" + ] + }, + "title": "Removing Default Blocks from Schema", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "block-manipulation", + "fullSlug": "basic/block-manipulation", + "pathFromRoot": "examples/01-basic/06-block-manipulation", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks" + ] + }, + "title": "Manipulating Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "selection-blocks", + "fullSlug": "basic/selection-blocks", + "pathFromRoot": "examples/01-basic/07-selection-blocks", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks" + ] + }, + "title": "Displaying Selected Blocks", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "ariakit", + "fullSlug": "basic/ariakit", + "pathFromRoot": "examples/01-basic/08-ariakit", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Use with Ariakit", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "shadcn", + "fullSlug": "basic/shadcn", + "pathFromRoot": "examples/01-basic/09-shadcn", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic" + ] + }, + "title": "Use with ShadCN", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "localization", + "fullSlug": "basic/localization", + "pathFromRoot": "examples/01-basic/10-localization", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic" + ] + }, + "title": "Localization (i18n)", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "custom-placeholder", + "fullSlug": "basic/custom-placeholder", + "pathFromRoot": "examples/01-basic/11-custom-placeholder", + "config": { + "playground": true, + "docs": true, + "author": "ezhil56x", + "tags": [ + "Basic" + ] + }, + "title": "Change placeholder text", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "multi-editor", + "fullSlug": "basic/multi-editor", + "pathFromRoot": "examples/01-basic/12-multi-editor", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "Basic" + ] + }, + "title": "Multi-Editor Setup", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "custom-paste-handler", + "fullSlug": "basic/custom-paste-handler", + "pathFromRoot": "examples/01-basic/13-custom-paste-handler", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" + ] + }, + "title": "Custom Paste Handler", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + }, + { + "projectSlug": "testing", + "fullSlug": "basic/testing", + "pathFromRoot": "examples/01-basic/testing", + "config": { + "playground": true, + "docs": false + }, + "title": "Test Editor", + "group": { + "pathFromRoot": "examples/01-basic", + "slug": "basic" + } + } + ] }, - backend: { - pathFromRoot: "examples/02-backend", - slug: "backend", - projects: [ - { - projectSlug: "file-uploading", - fullSlug: "backend/file-uploading", - pathFromRoot: "examples/02-backend/01-file-uploading", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Saving/Loading"], - }, - title: "Upload Files", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - }, - { - projectSlug: "saving-loading", - fullSlug: "backend/saving-loading", - pathFromRoot: "examples/02-backend/02-saving-loading", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Intermediate", "Blocks", "Saving/Loading"], - }, - title: "Saving & Loading", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - }, - { - projectSlug: "s3", - fullSlug: "backend/s3", - pathFromRoot: "examples/02-backend/03-s3", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Saving/Loading"], - dependencies: { + "backend": { + "pathFromRoot": "examples/02-backend", + "slug": "backend", + "projects": [ + { + "projectSlug": "file-uploading", + "fullSlug": "backend/file-uploading", + "pathFromRoot": "examples/02-backend/01-file-uploading", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Saving/Loading" + ] + }, + "title": "Upload Files", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + } + }, + { + "projectSlug": "saving-loading", + "fullSlug": "backend/saving-loading", + "pathFromRoot": "examples/02-backend/02-saving-loading", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Intermediate", + "Blocks", + "Saving/Loading" + ] + }, + "title": "Saving & Loading", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + } + }, + { + "projectSlug": "s3", + "fullSlug": "backend/s3", + "pathFromRoot": "examples/02-backend/03-s3", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Saving/Loading" + ], + "dependencies": { "@aws-sdk/client-s3": "^3.609.0", - "@aws-sdk/s3-request-presigner": "^3.609.0", - } as any, - pro: true, - }, - title: "Upload Files to AWS S3", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - }, - { - projectSlug: "rendering-static-documents", - fullSlug: "backend/rendering-static-documents", - pathFromRoot: "examples/02-backend/04-rendering-static-documents", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["server"], - dependencies: { - "@blocknote/server-util": "latest", + "@aws-sdk/s3-request-presigner": "^3.609.0" } as any, - }, - title: "Rendering static documents", - group: { - pathFromRoot: "examples/02-backend", - slug: "backend", - }, - }, - ], + "pro": true + }, + "title": "Upload Files to AWS S3", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + } + }, + { + "projectSlug": "rendering-static-documents", + "fullSlug": "backend/rendering-static-documents", + "pathFromRoot": "examples/02-backend/04-rendering-static-documents", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "server" + ], + "dependencies": { + "@blocknote/server-util": "latest" + } as any + }, + "title": "Rendering static documents", + "group": { + "pathFromRoot": "examples/02-backend", + "slug": "backend" + } + } + ] }, "ui-components": { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - projects: [ - { - projectSlug: "formatting-toolbar-buttons", - fullSlug: "ui-components/formatting-toolbar-buttons", - pathFromRoot: "examples/03-ui-components/02-formatting-toolbar-buttons", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components", + "projects": [ + { + "projectSlug": "formatting-toolbar-buttons", + "fullSlug": "ui-components/formatting-toolbar-buttons", + "pathFromRoot": "examples/03-ui-components/02-formatting-toolbar-buttons", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Inline Content", "UI Components", - "Formatting Toolbar", - ], - }, - title: "Adding Formatting Toolbar Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "formatting-toolbar-block-type-items", - fullSlug: "ui-components/formatting-toolbar-block-type-items", - pathFromRoot: - "examples/03-ui-components/03-formatting-toolbar-block-type-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "Formatting Toolbar" + ] + }, + "title": "Adding Formatting Toolbar Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "formatting-toolbar-block-type-items", + "fullSlug": "ui-components/formatting-toolbar-block-type-items", + "pathFromRoot": "examples/03-ui-components/03-formatting-toolbar-block-type-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "UI Components", "Formatting Toolbar", - "Custom Schemas", + "Custom Schemas" ], - dependencies: { + "dependencies": { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Block Type Select Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "side-menu-buttons", - fullSlug: "ui-components/side-menu-buttons", - pathFromRoot: "examples/03-ui-components/04-side-menu-buttons", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Block Side Menu Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "side-menu-drag-handle-items", - fullSlug: "ui-components/side-menu-drag-handle-items", - pathFromRoot: - "examples/03-ui-components/05-side-menu-drag-handle-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Intermediate", "Blocks", "UI Components", "Block Side Menu"], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Drag Handle Menu Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "suggestion-menus-slash-menu-items", - fullSlug: "ui-components/suggestion-menus-slash-menu-items", - pathFromRoot: - "examples/03-ui-components/06-suggestion-menus-slash-menu-items", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Block Type Select Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "side-menu-buttons", + "fullSlug": "ui-components/side-menu-buttons", + "pathFromRoot": "examples/03-ui-components/04-side-menu-buttons", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "UI Components", + "Block Side Menu" + ], + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Block Side Menu Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "side-menu-drag-handle-items", + "fullSlug": "ui-components/side-menu-drag-handle-items", + "pathFromRoot": "examples/03-ui-components/05-side-menu-drag-handle-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Intermediate", + "Blocks", + "UI Components", + "Block Side Menu" + ], + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Drag Handle Menu Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "suggestion-menus-slash-menu-items", + "fullSlug": "ui-components/suggestion-menus-slash-menu-items", + "pathFromRoot": "examples/03-ui-components/06-suggestion-menus-slash-menu-items", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Adding Slash Menu Items", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "suggestion-menus-slash-menu-component", - fullSlug: "ui-components/suggestion-menus-slash-menu-component", - pathFromRoot: - "examples/03-ui-components/07-suggestion-menus-slash-menu-component", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Adding Slash Menu Items", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "suggestion-menus-slash-menu-component", + "fullSlug": "ui-components/suggestion-menus-slash-menu-component", + "pathFromRoot": "examples/03-ui-components/07-suggestion-menus-slash-menu-component", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "UI Components", "Suggestion Menus", "Slash Menu", - "Appearance & Styling", - ], - }, - title: "Replacing Slash Menu Component", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "suggestion-menus-emoji-picker-columns", - fullSlug: "ui-components/suggestion-menus-emoji-picker-columns", - pathFromRoot: - "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "Appearance & Styling" + ] + }, + "title": "Replacing Slash Menu Component", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "suggestion-menus-emoji-picker-columns", + "fullSlug": "ui-components/suggestion-menus-emoji-picker-columns", + "pathFromRoot": "examples/03-ui-components/08-suggestion-menus-emoji-picker-columns", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Blocks", "UI Components", "Suggestion Menus", - "Emoji Picker", - ], - }, - title: "Changing Emoji Picker Columns", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "suggestion-menus-emoji-picker-component", - fullSlug: "ui-components/suggestion-menus-emoji-picker-component", - pathFromRoot: - "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "Emoji Picker" + ] + }, + "title": "Changing Emoji Picker Columns", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "suggestion-menus-emoji-picker-component", + "fullSlug": "ui-components/suggestion-menus-emoji-picker-component", + "pathFromRoot": "examples/03-ui-components/09-suggestion-menus-emoji-picker-component", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "UI Components", "Suggestion Menus", "Emoji Picker", - "Appearance & Styling", - ], - }, - title: "Replacing Emoji Picker Component", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "suggestion-menus-grid-mentions", - fullSlug: "ui-components/suggestion-menus-grid-mentions", - pathFromRoot: - "examples/03-ui-components/10-suggestion-menus-grid-mentions", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "Appearance & Styling" + ] + }, + "title": "Replacing Emoji Picker Component", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "suggestion-menus-grid-mentions", + "fullSlug": "ui-components/suggestion-menus-grid-mentions", + "pathFromRoot": "examples/03-ui-components/10-suggestion-menus-grid-mentions", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus", + "Suggestion Menus" + ] + }, + "title": "Grid Mentions Menu", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "uppy-file-panel", + "fullSlug": "ui-components/uppy-file-panel", + "pathFromRoot": "examples/03-ui-components/11-uppy-file-panel", + "config": { + "playground": true, + "docs": true, + "author": "ezhil56x", + "tags": [ + "Intermediate", + "Files" ], - }, - title: "Grid Mentions Menu", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "uppy-file-panel", - fullSlug: "ui-components/uppy-file-panel", - pathFromRoot: "examples/03-ui-components/11-uppy-file-panel", - config: { - playground: true, - docs: true, - author: "ezhil56x", - tags: ["Intermediate", "Files"], - dependencies: { + "dependencies": { "@uppy/core": "^3.13.1", "@uppy/dashboard": "^3.9.1", "@uppy/drag-drop": "^3.1.1", @@ -539,46 +592,46 @@ export const examples = { "@uppy/status-bar": "^3.1.1", "@uppy/webcam": "^3.4.2", "@uppy/xhr-upload": "^3.4.0", - "react-icons": "^5.2.1", + "react-icons": "^5.2.1" } as any, - pro: true, - }, - title: "Uppy File Panel", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "static-formatting-toolbar", - fullSlug: "ui-components/static-formatting-toolbar", - pathFromRoot: "examples/03-ui-components/12-static-formatting-toolbar", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pro": true + }, + "title": "Uppy File Panel", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "static-formatting-toolbar", + "fullSlug": "ui-components/static-formatting-toolbar", + "pathFromRoot": "examples/03-ui-components/12-static-formatting-toolbar", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Basic", "UI Components", "Formatting Toolbar", - "Appearance & Styling", - ], - }, - title: "Static Formatting Toolbar", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "custom-ui", - fullSlug: "ui-components/custom-ui", - pathFromRoot: "examples/03-ui-components/13-custom-ui", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "Appearance & Styling" + ] + }, + "title": "Static Formatting Toolbar", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "custom-ui", + "fullSlug": "ui-components/custom-ui", + "pathFromRoot": "examples/03-ui-components/13-custom-ui", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Advanced", "Inline Content", "UI Components", @@ -586,884 +639,965 @@ export const examples = { "Formatting Toolbar", "Suggestion Menus", "Slash Menu", - "Appearance & Styling", + "Appearance & Styling" ], - dependencies: { + "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.16.1", - "@mui/material": "^5.16.1", + "@mui/material": "^5.16.1" } as any, - pro: true, - }, - title: "UI With Third-Party Components", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "experimental-mobile-formatting-toolbar", - fullSlug: "ui-components/experimental-mobile-formatting-toolbar", - pathFromRoot: - "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: [ + "pro": true + }, + "title": "UI With Third-Party Components", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "experimental-mobile-formatting-toolbar", + "fullSlug": "ui-components/experimental-mobile-formatting-toolbar", + "pathFromRoot": "examples/03-ui-components/14-experimental-mobile-formatting-toolbar", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ "Intermediate", "UI Components", "Formatting Toolbar", - "Appearance & Styling", - ], - }, - title: "Experimental Mobile Formatting Toolbar", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "advanced-tables", - fullSlug: "ui-components/advanced-tables", - pathFromRoot: "examples/03-ui-components/15-advanced-tables", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: [ + "Appearance & Styling" + ] + }, + "title": "Experimental Mobile Formatting Toolbar", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "advanced-tables", + "fullSlug": "ui-components/advanced-tables", + "pathFromRoot": "examples/03-ui-components/15-advanced-tables", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ "Intermediate", "UI Components", "Tables", + "Appearance & Styling" + ] + }, + "title": "Advanced Tables", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + }, + { + "projectSlug": "link-toolbar-buttons", + "fullSlug": "ui-components/link-toolbar-buttons", + "pathFromRoot": "examples/03-ui-components/link-toolbar-buttons", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Adding Link Toolbar Buttons", + "group": { + "pathFromRoot": "examples/03-ui-components", + "slug": "ui-components" + } + } + ] + }, + "theming": { + "pathFromRoot": "examples/04-theming", + "slug": "theming", + "projects": [ + { + "projectSlug": "theming-dom-attributes", + "fullSlug": "theming/theming-dom-attributes", + "pathFromRoot": "examples/04-theming/01-theming-dom-attributes", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Appearance & Styling" + ] + }, + "title": "Adding CSS Class to Blocks", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "changing-font", + "fullSlug": "theming/changing-font", + "pathFromRoot": "examples/04-theming/02-changing-font", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling" + ] + }, + "title": "Changing Editor Font", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "theming-css", + "fullSlug": "theming/theming-css", + "pathFromRoot": "examples/04-theming/03-theming-css", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling", + "UI Components" + ] + }, + "title": "Overriding CSS Styles", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "theming-css-variables", + "fullSlug": "theming/theming-css-variables", + "pathFromRoot": "examples/04-theming/04-theming-css-variables", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Appearance & Styling", + "UI Components" + ] + }, + "title": "Overriding Theme CSS Variables", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "theming-css-variables-code", + "fullSlug": "theming/theming-css-variables-code", + "pathFromRoot": "examples/04-theming/05-theming-css-variables-code", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", "Appearance & Styling", + "UI Components" + ] + }, + "title": "Changing Themes Through Code", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "code-block", + "fullSlug": "theming/code-block", + "pathFromRoot": "examples/04-theming/06-code-block", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" ], - }, - title: "Advanced Tables", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - { - projectSlug: "link-toolbar-buttons", - fullSlug: "ui-components/link-toolbar-buttons", - pathFromRoot: "examples/03-ui-components/link-toolbar-buttons", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Adding Link Toolbar Buttons", - group: { - pathFromRoot: "examples/03-ui-components", - slug: "ui-components", - }, - }, - ], - }, - theming: { - pathFromRoot: "examples/04-theming", - slug: "theming", - projects: [ - { - projectSlug: "theming-dom-attributes", - fullSlug: "theming/theming-dom-attributes", - pathFromRoot: "examples/04-theming/01-theming-dom-attributes", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Appearance & Styling"], - }, - title: "Adding CSS Class to Blocks", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "changing-font", - fullSlug: "theming/changing-font", - pathFromRoot: "examples/04-theming/02-changing-font", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling"], - }, - title: "Changing Editor Font", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "theming-css", - fullSlug: "theming/theming-css", - pathFromRoot: "examples/04-theming/03-theming-css", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Overriding CSS Styles", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "theming-css-variables", - fullSlug: "theming/theming-css-variables", - pathFromRoot: "examples/04-theming/04-theming-css-variables", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Overriding Theme CSS Variables", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "theming-css-variables-code", - fullSlug: "theming/theming-css-variables-code", - pathFromRoot: "examples/04-theming/05-theming-css-variables-code", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Appearance & Styling", "UI Components"], - }, - title: "Changing Themes Through Code", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "code-block", - fullSlug: "theming/code-block", - pathFromRoot: "examples/04-theming/06-code-block", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - dependencies: { - "@blocknote/code-block": "latest", - } as any, - }, - title: "Code Block Syntax Highlighting", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - { - projectSlug: "custom-code-block", - fullSlug: "theming/custom-code-block", - pathFromRoot: "examples/04-theming/07-custom-code-block", - config: { - playground: true, - docs: true, - author: "nperez0111", - tags: ["Basic"], - dependencies: { + "dependencies": { + "@blocknote/code-block": "latest" + } as any + }, + "title": "Code Block Syntax Highlighting", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + }, + { + "projectSlug": "custom-code-block", + "fullSlug": "theming/custom-code-block", + "pathFromRoot": "examples/04-theming/07-custom-code-block", + "config": { + "playground": true, + "docs": true, + "author": "nperez0111", + "tags": [ + "Basic" + ], + "dependencies": { "@blocknote/code-block": "latest", "@shikijs/types": "^3.2.1", "@shikijs/core": "^3.2.1", "@shikijs/engine-javascript": "^3.2.1", "@shikijs/langs-precompiled": "^3.2.1", - "@shikijs/themes": "^3.2.1", - } as any, - }, - title: "Custom Code Block Theme & Language", - group: { - pathFromRoot: "examples/04-theming", - slug: "theming", - }, - }, - ], + "@shikijs/themes": "^3.2.1" + } as any + }, + "title": "Custom Code Block Theme & Language", + "group": { + "pathFromRoot": "examples/04-theming", + "slug": "theming" + } + } + ] }, - interoperability: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - projects: [ - { - projectSlug: "converting-blocks-to-html", - fullSlug: "interoperability/converting-blocks-to-html", - pathFromRoot: - "examples/05-interoperability/01-converting-blocks-to-html", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Converting Blocks to HTML", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-from-html", - fullSlug: "interoperability/converting-blocks-from-html", - pathFromRoot: - "examples/05-interoperability/02-converting-blocks-from-html", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Parsing HTML to Blocks", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-to-md", - fullSlug: "interoperability/converting-blocks-to-md", - pathFromRoot: "examples/05-interoperability/03-converting-blocks-to-md", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Converting Blocks to Markdown", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-from-md", - fullSlug: "interoperability/converting-blocks-from-md", - pathFromRoot: - "examples/05-interoperability/04-converting-blocks-from-md", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Basic", "Blocks", "Import/Export"], - }, - title: "Parsing Markdown to Blocks", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-to-pdf", - fullSlug: "interoperability/converting-blocks-to-pdf", - pathFromRoot: - "examples/05-interoperability/05-converting-blocks-to-pdf", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Interoperability"], - dependencies: { + "interoperability": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability", + "projects": [ + { + "projectSlug": "converting-blocks-to-html", + "fullSlug": "interoperability/converting-blocks-to-html", + "pathFromRoot": "examples/05-interoperability/01-converting-blocks-to-html", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Converting Blocks to HTML", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-from-html", + "fullSlug": "interoperability/converting-blocks-from-html", + "pathFromRoot": "examples/05-interoperability/02-converting-blocks-from-html", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Parsing HTML to Blocks", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-to-md", + "fullSlug": "interoperability/converting-blocks-to-md", + "pathFromRoot": "examples/05-interoperability/03-converting-blocks-to-md", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Converting Blocks to Markdown", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-from-md", + "fullSlug": "interoperability/converting-blocks-from-md", + "pathFromRoot": "examples/05-interoperability/04-converting-blocks-from-md", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Basic", + "Blocks", + "Import/Export" + ] + }, + "title": "Parsing Markdown to Blocks", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-to-pdf", + "fullSlug": "interoperability/converting-blocks-to-pdf", + "pathFromRoot": "examples/05-interoperability/05-converting-blocks-to-pdf", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Interoperability" + ], + "dependencies": { "@blocknote/xl-pdf-exporter": "latest", - "@react-pdf/renderer": "^4.3.0", + "@react-pdf/renderer": "^4.3.0" } as any, - pro: true, - }, - title: "Exporting documents to PDF", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-to-docx", - fullSlug: "interoperability/converting-blocks-to-docx", - pathFromRoot: - "examples/05-interoperability/06-converting-blocks-to-docx", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [""], - dependencies: { + "pro": true + }, + "title": "Exporting documents to PDF", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-to-docx", + "fullSlug": "interoperability/converting-blocks-to-docx", + "pathFromRoot": "examples/05-interoperability/06-converting-blocks-to-docx", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "" + ], + "dependencies": { "@blocknote/xl-docx-exporter": "latest", - docx: "^9.0.2", + "docx": "^9.0.2" } as any, - pro: true, - }, - title: "Exporting documents to .docx (Office Open XML)", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-to-odt", - fullSlug: "interoperability/converting-blocks-to-odt", - pathFromRoot: - "examples/05-interoperability/07-converting-blocks-to-odt", - config: { - playground: true, - docs: true, - author: "areknawo", - tags: [""], - dependencies: { - "@blocknote/xl-odt-exporter": "latest", + "pro": true + }, + "title": "Exporting documents to .docx (Office Open XML)", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-to-odt", + "fullSlug": "interoperability/converting-blocks-to-odt", + "pathFromRoot": "examples/05-interoperability/07-converting-blocks-to-odt", + "config": { + "playground": true, + "docs": true, + "author": "areknawo", + "tags": [ + "" + ], + "dependencies": { + "@blocknote/xl-odt-exporter": "latest" } as any, - pro: true, - }, - title: "Exporting documents to .odt (Open Document Text)", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - { - projectSlug: "converting-blocks-to-react-email", - fullSlug: "interoperability/converting-blocks-to-react-email", - pathFromRoot: - "examples/05-interoperability/08-converting-blocks-to-react-email", - config: { - playground: true, - docs: true, - author: "jmarbutt", - tags: [""], - dependencies: { + "pro": true + }, + "title": "Exporting documents to .odt (Open Document Text)", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + }, + { + "projectSlug": "converting-blocks-to-react-email", + "fullSlug": "interoperability/converting-blocks-to-react-email", + "pathFromRoot": "examples/05-interoperability/08-converting-blocks-to-react-email", + "config": { + "playground": true, + "docs": true, + "author": "jmarbutt", + "tags": [ + "" + ], + "dependencies": { "@blocknote/xl-email-exporter": "latest", - "@react-email/render": "^1.1.2", + "@react-email/render": "^1.1.2" } as any, - pro: true, - }, - title: "Exporting documents to React Email", - group: { - pathFromRoot: "examples/05-interoperability", - slug: "interoperability", - }, - }, - ], + "pro": true + }, + "title": "Exporting documents to React Email", + "group": { + "pathFromRoot": "examples/05-interoperability", + "slug": "interoperability" + } + } + ] }, "custom-schema": { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - projects: [ - { - projectSlug: "alert-block", - fullSlug: "custom-schema/alert-block", - pathFromRoot: "examples/06-custom-schema/01-alert-block", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema", + "projects": [ + { + "projectSlug": "alert-block", + "fullSlug": "custom-schema/alert-block", + "pathFromRoot": "examples/06-custom-schema/01-alert-block", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1", - } as any, - }, - title: "Alert Block", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "suggestion-menus-mentions", - fullSlug: "custom-schema/suggestion-menus-mentions", - pathFromRoot: "examples/06-custom-schema/02-suggestion-menus-mentions", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: [ + "react-icons": "^5.2.1" + } as any + }, + "title": "Alert Block", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "suggestion-menus-mentions", + "fullSlug": "custom-schema/suggestion-menus-mentions", + "pathFromRoot": "examples/06-custom-schema/02-suggestion-menus-mentions", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Suggestion Menus", - ], - }, - title: "Mentions Menu", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "font-style", - fullSlug: "custom-schema/font-style", - pathFromRoot: "examples/06-custom-schema/03-font-style", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "Suggestion Menus" + ] + }, + "title": "Mentions Menu", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "font-style", + "fullSlug": "custom-schema/font-style", + "pathFromRoot": "examples/06-custom-schema/03-font-style", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Inline Content", "Custom Schemas", - "Formatting Toolbar", + "Formatting Toolbar" ], - dependencies: { - "react-icons": "^5.2.1", - } as any, - }, - title: "Font Style", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "pdf-file-block", - fullSlug: "custom-schema/pdf-file-block", - pathFromRoot: "examples/06-custom-schema/04-pdf-file-block", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "dependencies": { + "react-icons": "^5.2.1" + } as any + }, + "title": "Font Style", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "pdf-file-block", + "fullSlug": "custom-schema/pdf-file-block", + "pathFromRoot": "examples/06-custom-schema/04-pdf-file-block", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1", + "react-icons": "^5.2.1" } as any, - pro: true, - }, - title: "PDF Block", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "alert-block-full-ux", - fullSlug: "custom-schema/alert-block-full-ux", - pathFromRoot: "examples/06-custom-schema/05-alert-block-full-ux", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: [ + "pro": true + }, + "title": "PDF Block", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "alert-block-full-ux", + "fullSlug": "custom-schema/alert-block-full-ux", + "pathFromRoot": "examples/06-custom-schema/05-alert-block-full-ux", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ "Intermediate", "Blocks", "Custom Schemas", "Formatting Toolbar", "Suggestion Menus", - "Slash Menu", + "Slash Menu" ], - dependencies: { + "dependencies": { "@mantine/core": "^7.10.1", - "react-icons": "^5.2.1", - } as any, - }, - title: "Alert Block with Full UX", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "react-custom-blocks", - fullSlug: "custom-schema/react-custom-blocks", - pathFromRoot: "examples/06-custom-schema/react-custom-blocks", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Blocks - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "react-custom-inline-content", - fullSlug: "custom-schema/react-custom-inline-content", - pathFromRoot: "examples/06-custom-schema/react-custom-inline-content", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Inline Content - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - { - projectSlug: "react-custom-styles", - fullSlug: "custom-schema/react-custom-styles", - pathFromRoot: "examples/06-custom-schema/react-custom-styles", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Styles - React API", - group: { - pathFromRoot: "examples/06-custom-schema", - slug: "custom-schema", - }, - }, - ], + "react-icons": "^5.2.1" + } as any + }, + "title": "Alert Block with Full UX", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "react-custom-blocks", + "fullSlug": "custom-schema/react-custom-blocks", + "pathFromRoot": "examples/06-custom-schema/react-custom-blocks", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Blocks - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "react-custom-inline-content", + "fullSlug": "custom-schema/react-custom-inline-content", + "pathFromRoot": "examples/06-custom-schema/react-custom-inline-content", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Inline Content - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + }, + { + "projectSlug": "react-custom-styles", + "fullSlug": "custom-schema/react-custom-styles", + "pathFromRoot": "examples/06-custom-schema/react-custom-styles", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Styles - React API", + "group": { + "pathFromRoot": "examples/06-custom-schema", + "slug": "custom-schema" + } + } + ] }, - collaboration: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - projects: [ - { - projectSlug: "partykit", - fullSlug: "collaboration/partykit", - pathFromRoot: "examples/07-collaboration/01-partykit", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { + "collaboration": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration", + "projects": [ + { + "projectSlug": "partykit", + "fullSlug": "collaboration/partykit", + "pathFromRoot": "examples/07-collaboration/01-partykit", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.15", - } as any, - }, - title: "Collaborative Editing with PartyKit", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "liveblocks", - fullSlug: "collaboration/liveblocks", - pathFromRoot: "examples/07-collaboration/02-liveblocks", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { + "yjs": "^13.6.15" + } as any + }, + "title": "Collaborative Editing with PartyKit", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "liveblocks", + "fullSlug": "collaboration/liveblocks", + "pathFromRoot": "examples/07-collaboration/02-liveblocks", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { "@liveblocks/client": "^2.23.1", "@liveblocks/react": "^2.23.1", "@liveblocks/react-blocknote": "^2.23.1", "@liveblocks/react-tiptap": "^2.23.1", "@liveblocks/react-ui": "^2.23.1", - yjs: "^13.6.15", - } as any, - }, - title: "Collaborative Editing with Liveblocks", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "y-sweet", - fullSlug: "collaboration/y-sweet", - pathFromRoot: "examples/07-collaboration/03-y-sweet", - config: { - playground: true, - docs: true, - author: "jakelazaroff", - tags: ["Advanced", "Saving/Loading", "Collaboration"], - dependencies: { - "@y-sweet/react": "^0.6.3", - } as any, - }, - title: "Collaborative Editing with Y-Sweet", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "comments", - fullSlug: "collaboration/comments", - pathFromRoot: "examples/07-collaboration/04-comments", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["Advanced", "Comments", "Collaboration"], - dependencies: { + "yjs": "^13.6.15" + } as any + }, + "title": "Collaborative Editing with Liveblocks", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "y-sweet", + "fullSlug": "collaboration/y-sweet", + "pathFromRoot": "examples/07-collaboration/03-y-sweet", + "config": { + "playground": true, + "docs": true, + "author": "jakelazaroff", + "tags": [ + "Advanced", + "Saving/Loading", + "Collaboration" + ], + "dependencies": { + "@y-sweet/react": "^0.6.3" + } as any + }, + "title": "Collaborative Editing with Y-Sweet", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "comments", + "fullSlug": "collaboration/comments", + "pathFromRoot": "examples/07-collaboration/04-comments", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "Advanced", + "Comments", + "Collaboration" + ], + "dependencies": { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^7.10.1", - } as any, - }, - title: "Comments & Threads", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "comments-with-sidebar", - fullSlug: "collaboration/comments-with-sidebar", - pathFromRoot: "examples/07-collaboration/05-comments-with-sidebar", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["Advanced", "Comments", "Collaboration"], - dependencies: { + "@mantine/core": "^7.10.1" + } as any + }, + "title": "Comments & Threads", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "comments-with-sidebar", + "fullSlug": "collaboration/comments-with-sidebar", + "pathFromRoot": "examples/07-collaboration/05-comments-with-sidebar", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "Advanced", + "Comments", + "Collaboration" + ], + "dependencies": { "@y-sweet/react": "^0.6.3", - "@mantine/core": "^7.10.1", - } as any, - }, - title: "Threads Sidebar", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "ghost-writer", - fullSlug: "collaboration/ghost-writer", - pathFromRoot: "examples/07-collaboration/06-ghost-writer", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["Advanced", "Development", "Collaboration"], - dependencies: { + "@mantine/core": "^7.10.1" + } as any + }, + "title": "Threads Sidebar", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "ghost-writer", + "fullSlug": "collaboration/ghost-writer", + "pathFromRoot": "examples/07-collaboration/06-ghost-writer", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "Advanced", + "Development", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.15", - } as any, - }, - title: "Ghost Writer", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - { - projectSlug: "forking", - fullSlug: "collaboration/forking", - pathFromRoot: "examples/07-collaboration/07-forking", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["Advanced", "Development", "Collaboration"], - dependencies: { + "yjs": "^13.6.15" + } as any + }, + "title": "Ghost Writer", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + }, + { + "projectSlug": "forking", + "fullSlug": "collaboration/forking", + "pathFromRoot": "examples/07-collaboration/07-forking", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "Advanced", + "Development", + "Collaboration" + ], + "dependencies": { "y-partykit": "^0.0.25", - yjs: "^13.6.15", - } as any, - }, - title: "Collaborative Editing with Forking", - group: { - pathFromRoot: "examples/07-collaboration", - slug: "collaboration", - }, - }, - ], + "yjs": "^13.6.15" + } as any + }, + "title": "Collaborative Editing with Forking", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + } + } + ] }, - extensions: { - pathFromRoot: "examples/08-extensions", - slug: "extensions", - projects: [ - { - projectSlug: "tiptap-arrow-conversion", - fullSlug: "extensions/tiptap-arrow-conversion", - pathFromRoot: "examples/08-extensions/01-tiptap-arrow-conversion", - config: { - playground: true, - docs: true, - author: "komsenapati", - tags: ["Extension"], - pro: true, - dependencies: { - "@tiptap/core": "^2.12.0", - } as any, - }, - title: "TipTap extension (arrow InputRule)", - group: { - pathFromRoot: "examples/08-extensions", - slug: "extensions", - }, - }, - ], + "extensions": { + "pathFromRoot": "examples/08-extensions", + "slug": "extensions", + "projects": [ + { + "projectSlug": "tiptap-arrow-conversion", + "fullSlug": "extensions/tiptap-arrow-conversion", + "pathFromRoot": "examples/08-extensions/01-tiptap-arrow-conversion", + "config": { + "playground": true, + "docs": true, + "author": "komsenapati", + "tags": [ + "Extension" + ], + "pro": true, + "dependencies": { + "@tiptap/core": "^2.12.0" + } as any + }, + "title": "TipTap extension (arrow InputRule)", + "group": { + "pathFromRoot": "examples/08-extensions", + "slug": "extensions" + } + } + ] }, - ai: { - pathFromRoot: "examples/09-ai", - slug: "ai", - projects: [ - { - projectSlug: "minimal", - fullSlug: "ai/minimal", - pathFromRoot: "examples/09-ai/01-minimal", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "ai": { + "pathFromRoot": "examples/09-ai", + "slug": "ai", + "projects": [ + { + "projectSlug": "minimal", + "fullSlug": "ai/minimal", + "pathFromRoot": "examples/09-ai/01-minimal", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - ai: "^4.3.15", + "ai": "^4.3.15", "@ai-sdk/groq": "^1.2.9", - zustand: "^5.0.3", - } as any, - }, - title: "Rich Text editor AI integration", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - }, - { - projectSlug: "playground", - fullSlug: "ai/playground", - pathFromRoot: "examples/09-ai/02-playground", - config: { - playground: true, - docs: true, - author: "yousefed", - tags: ["AI", "llm"], - dependencies: { + "zustand": "^5.0.3" + } as any + }, + "title": "Rich Text editor AI integration", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + } + }, + { + "projectSlug": "playground", + "fullSlug": "ai/playground", + "pathFromRoot": "examples/09-ai/02-playground", + "config": { + "playground": true, + "docs": true, + "author": "yousefed", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - ai: "^4.3.15", + "ai": "^4.3.15", "@ai-sdk/openai": "^1.3.22", "@ai-sdk/openai-compatible": "^0.2.14", "@ai-sdk/groq": "^1.2.9", "@ai-sdk/anthropic": "^1.2.11", "@ai-sdk/mistral": "^1.2.8", - zustand: "^5.0.3", - } as any, - }, - title: "AI Playground", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - }, - { - projectSlug: "custom-ai-menu-items", - fullSlug: "ai/custom-ai-menu-items", - pathFromRoot: "examples/09-ai/03-custom-ai-menu-items", - config: { - playground: true, - docs: true, - author: "matthewlipski", - tags: ["AI", "llm"], - dependencies: { + "zustand": "^5.0.3" + } as any + }, + "title": "AI Playground", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + } + }, + { + "projectSlug": "custom-ai-menu-items", + "fullSlug": "ai/custom-ai-menu-items", + "pathFromRoot": "examples/09-ai/03-custom-ai-menu-items", + "config": { + "playground": true, + "docs": true, + "author": "matthewlipski", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - ai: "^4.1.0", + "ai": "^4.1.0", "@ai-sdk/openai": "^1.1.0", "@ai-sdk/groq": "^1.1.0", "react-icons": "^5.2.1", - zustand: "^5.0.3", - } as any, - }, - title: "Adding AI Menu Items", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - }, - { - projectSlug: "with-collaboration", - fullSlug: "ai/with-collaboration", - pathFromRoot: "examples/09-ai/04-with-collaboration", - config: { - playground: true, - docs: false, - author: "nperez0111", - tags: ["AI", "llm"], - dependencies: { + "zustand": "^5.0.3" + } as any + }, + "title": "Adding AI Menu Items", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + } + }, + { + "projectSlug": "with-collaboration", + "fullSlug": "ai/with-collaboration", + "pathFromRoot": "examples/09-ai/04-with-collaboration", + "config": { + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": [ + "AI", + "llm" + ], + "dependencies": { "@blocknote/xl-ai": "latest", "@mantine/core": "^7.10.1", - ai: "^4.3.15", + "ai": "^4.3.15", "@ai-sdk/groq": "^1.2.9", "y-partykit": "^0.0.25", - yjs: "^13.6.15", - zustand: "^5.0.3", - } as any, - }, - title: "AI + Ghost Writer", - group: { - pathFromRoot: "examples/09-ai", - slug: "ai", - }, - }, - ], + "yjs": "^13.6.15", + "zustand": "^5.0.3" + } as any + }, + "title": "AI + Ghost Writer", + "group": { + "pathFromRoot": "examples/09-ai", + "slug": "ai" + } + } + ] }, "vanilla-js": { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - projects: [ - { - projectSlug: "react-vanilla-custom-blocks", - fullSlug: "vanilla-js/react-vanilla-custom-blocks", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-blocks", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Blocks - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - }, - { - projectSlug: "react-vanilla-custom-inline-content", - fullSlug: "vanilla-js/react-vanilla-custom-inline-content", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-inline-content", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Inline Content - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - }, - { - projectSlug: "react-vanilla-custom-styles", - fullSlug: "vanilla-js/react-vanilla-custom-styles", - pathFromRoot: "examples/vanilla-js/react-vanilla-custom-styles", - config: { - playground: true, - docs: false, - author: "matthewlipski", - tags: [], - }, - title: "Custom Styles - Vanilla JS API", - group: { - pathFromRoot: "examples/vanilla-js", - slug: "vanilla-js", - }, - }, - ], - }, -}; + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js", + "projects": [ + { + "projectSlug": "react-vanilla-custom-blocks", + "fullSlug": "vanilla-js/react-vanilla-custom-blocks", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-blocks", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Blocks - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + } + }, + { + "projectSlug": "react-vanilla-custom-inline-content", + "fullSlug": "vanilla-js/react-vanilla-custom-inline-content", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-inline-content", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Inline Content - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + } + }, + { + "projectSlug": "react-vanilla-custom-styles", + "fullSlug": "vanilla-js/react-vanilla-custom-styles", + "pathFromRoot": "examples/vanilla-js/react-vanilla-custom-styles", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [] + }, + "title": "Custom Styles - Vanilla JS API", + "group": { + "pathFromRoot": "examples/vanilla-js", + "slug": "vanilla-js" + } + } + ] + } +}; \ No newline at end of file diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 69075ccbe9..5e675e4f64 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -29,6 +29,7 @@ { "path": "../packages/xl-pdf-exporter/" }, { "path": "../packages/xl-odt-exporter/" }, { "path": "../packages/xl-docx-exporter/" }, - { "path": "../packages/xl-multi-column/" } + { "path": "../packages/xl-multi-column/" }, + { "path": "../packages/xl-email-exporter/" } ] } diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 10455b2d4c..c3586999fa 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -62,7 +62,7 @@ export default defineConfig((conf) => ({ ), "@blocknote/xl-email-exporter": resolve( __dirname, - "../packages/xl-react-email-exporter/src", + "../packages/xl-email-exporter/src", ), /* This can be used when developing against a local version of liveblocks: From f99fd21435170bc31645ee9404500adabf74733d Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 12:41:02 +0200 Subject: [PATCH 37/38] chore: must please the linter --- packages/xl-email-exporter/package.json | 2 +- .../src/react-email/defaultSchema/blocks.tsx | 4 +++- .../xl-email-exporter/src/react-email/defaultSchema/index.ts | 4 ++-- packages/xl-email-exporter/src/react-email/index.ts | 4 ++-- .../src/react-email/reactEmailExporter.test.tsx | 4 ++-- .../xl-email-exporter/src/react-email/reactEmailExporter.tsx | 2 -- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/xl-email-exporter/package.json b/packages/xl-email-exporter/package.json index 2e55ceef71..626507c9fa 100644 --- a/packages/xl-email-exporter/package.json +++ b/packages/xl-email-exporter/package.json @@ -80,7 +80,7 @@ }, "eslintConfig": { "extends": [ - "../../.eslintrc.js" + "../../.eslintrc.json" ] }, "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" diff --git a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx index 1585fa4a22..2ccfa4162e 100644 --- a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -335,7 +335,9 @@ function FileLink({ } function Caption({ caption, width }: { caption?: string; width?: number }) { - if (!caption) return null; + if (!caption) { + return null; + } return ( {caption} diff --git a/packages/xl-email-exporter/src/react-email/defaultSchema/index.ts b/packages/xl-email-exporter/src/react-email/defaultSchema/index.ts index 0e7e4bf0f1..20b049705d 100644 --- a/packages/xl-email-exporter/src/react-email/defaultSchema/index.ts +++ b/packages/xl-email-exporter/src/react-email/defaultSchema/index.ts @@ -1,6 +1,6 @@ import { reactEmailBlockMappingForDefaultSchema } from "./blocks.js"; -import { reactEmailInlineContentMappingForDefaultSchema } from "./inlinecontent"; -import { reactEmailStyleMappingForDefaultSchema } from "./styles"; +import { reactEmailInlineContentMappingForDefaultSchema } from "./inlinecontent.js"; +import { reactEmailStyleMappingForDefaultSchema } from "./styles.js"; export const reactEmailDefaultSchemaMappings = { blockMapping: reactEmailBlockMappingForDefaultSchema, diff --git a/packages/xl-email-exporter/src/react-email/index.ts b/packages/xl-email-exporter/src/react-email/index.ts index 6d652034be..8412da0065 100644 --- a/packages/xl-email-exporter/src/react-email/index.ts +++ b/packages/xl-email-exporter/src/react-email/index.ts @@ -1,2 +1,2 @@ -export * from "./defaultSchema/index"; -export * from "./reactEmailExporter"; +export * from "./defaultSchema/index.js"; +export * from "./reactEmailExporter.jsx"; diff --git a/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx b/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx index e23261d5c2..648a1b4ad8 100644 --- a/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx +++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { ReactEmailExporter } from "./reactEmailExporter"; -import { reactEmailDefaultSchemaMappings } from "./defaultSchema"; +import { ReactEmailExporter } from "./reactEmailExporter.jsx"; +import { reactEmailDefaultSchemaMappings } from "./defaultSchema/index.js"; import { BlockNoteSchema, createBlockSpec, diff --git a/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx index b85144f50a..741576f0fb 100644 --- a/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx @@ -10,7 +10,6 @@ import { StyleSchema, StyledText, } from "@blocknote/core"; - import { Body, Container, @@ -22,7 +21,6 @@ import { Tailwind, } from "@react-email/components"; import { render as renderEmail } from "@react-email/render"; - import React, { CSSProperties } from "react"; export class ReactEmailExporter< From 00d17787b41df357c96ba70a38835d3ed98d5969 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 24 Jun 2025 12:46:59 +0200 Subject: [PATCH 38/38] feat: add support for toggleListItems --- .../src/react-email/defaultSchema/blocks.tsx | 4 ++++ .../xl-email-exporter/src/react-email/reactEmailExporter.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx index 2ccfa4162e..91f2f8ea71 100644 --- a/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx +++ b/packages/xl-email-exporter/src/react-email/defaultSchema/blocks.tsx @@ -29,6 +29,10 @@ export const reactEmailBlockMappingForDefaultSchema: BlockMapping< // Return only the
                          11. for grouping in the exporter return {t.transformInlineContent(block.content)}; }, + toggleListItem: (block, t) => { + // Return only the
                          12. for grouping in the exporter + return {t.transformInlineContent(block.content)}; + }, numberedListItem: (block, t, _nestingLevel) => { // Return only the
                          13. for grouping in the exporter return {t.transformInlineContent(block.content)}; diff --git a/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx index 741576f0fb..493f5bf20f 100644 --- a/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx +++ b/packages/xl-email-exporter/src/react-email/reactEmailExporter.tsx @@ -102,7 +102,7 @@ export class ReactEmailExporter< ); } let element: React.ReactElement; - if (listType === "bulletListItem") { + if (listType === "bulletListItem" || listType === "toggleListItem") { element = (
                              {listItems}