From 37dbe7dda4d1a3659c7ae986780457a960434989 Mon Sep 17 00:00:00 2001
From: Adeel Raza
Date: Sun, 4 Oct 2026 09:47:53 +0400
Subject: [PATCH 1/5] docs: add a runnable Elements-to-editor workflow
---
.github/workflows/ci.yml | 19 +
README.md | 58 +-
context7.json | 3 +-
demo/README.md | 41 +
demo/index.tsx | 10 +
demo/package-lock.json | 1701 +++++++++++++++++++++++++++--
demo/package.json | 13 +-
demo/scripts/check-docs.mjs | 49 +
demo/src/dashboard/DesignEdit.tsx | 8 +-
demo/src/elements/index.tsx | 188 ++++
demo/src/elements/storage.ts | 29 +
demo/src/elements/style.css | 114 ++
demo/src/elements/welcome.tsx | 40 +
demo/src/example/index.tsx | 49 +-
demo/test/elements.test.tsx | 204 ++++
demo/tsconfig.json | 9 +-
demo/vite-env.d.ts | 1 +
demo/vite.config.ts | 4 +-
demo/vitest.config.ts | 8 +
docs/react-elements.md | 121 ++
20 files changed, 2534 insertions(+), 135 deletions(-)
create mode 100644 demo/README.md
create mode 100644 demo/scripts/check-docs.mjs
create mode 100644 demo/src/elements/index.tsx
create mode 100644 demo/src/elements/storage.ts
create mode 100644 demo/src/elements/style.css
create mode 100644 demo/src/elements/welcome.tsx
create mode 100644 demo/test/elements.test.tsx
create mode 100644 demo/vite-env.d.ts
create mode 100644 demo/vitest.config.ts
create mode 100644 docs/react-elements.md
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba9a8b25e..39c689dfe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,6 +26,25 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
+ demo:
+ name: Elements demo and documentation examples
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ demo/package-lock.json
+ - run: npm ci
+ - run: npm run build
+ - run: npm --prefix demo ci
+ - run: npm --prefix demo run build
+ - run: npm --prefix demo test
+ - run: npm --prefix demo run test:docs
+
test:
name: Test (Node ${{ matrix.node }}, React ${{ matrix.react }})
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 6f292acc7..4dcd5462b 100644
--- a/README.md
+++ b/README.md
@@ -54,40 +54,42 @@ See the [CHANGELOG](CHANGELOG.md) for the full list.
Require the EmailEditor component and render it with JSX:
```tsx
-import React, { useRef } from 'react';
+import { useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
+import EmailEditor, {
+ type EditorRef,
+ type EmailEditorProps,
+} from 'react-email-editor';
-import EmailEditor, { EditorRef, EmailEditorProps } from 'react-email-editor';
-
-const App = (props) => {
+const App = () => {
const emailEditorRef = useRef(null);
+ const [ready, setReady] = useState(false);
+ const [html, setHtml] = useState('');
const exportHtml = () => {
- const unlayer = emailEditorRef.current?.editor;
-
- unlayer?.exportHtml((data) => {
- const { design, html } = data;
- console.log('exportHtml', html);
+ if (!ready) return;
+ emailEditorRef.current?.editor?.exportHtml((data) => {
+ setHtml(data.html);
+ // Persist data.design to reopen it later with editor.loadDesign(data.design).
});
};
- const onReady: EmailEditorProps['onReady'] = (unlayer) => {
- // editor is ready
- // you can load your template here;
- // the design json can be obtained by calling
- // unlayer.loadDesign(callback) or unlayer.exportHtml(callback)
- // or generated from React code with @unlayer/react-elements (renderToJson)
- // const templateJson = { DESIGN JSON GOES HERE };
- // unlayer.loadDesign(templateJson);
+ const onReady: EmailEditorProps['onReady'] = (editor) => {
+ setReady(true);
+ // editor is now ready for calls such as saveDesign and loadDesign.
+ console.log('Editor ready', editor);
+ // Load an existing design here: editor.loadDesign(savedDesign).
+ // Obtain design JSON from editor.saveDesign(callback), editor.exportHtml(callback),
+ // or @unlayer/react-elements' renderToJson(). loadDesign accepts JSON, not a callback.
};
return (
-
- Export HTML
-
-
+
+ Export HTML
+
+ {html &&
}
);
};
@@ -95,6 +97,8 @@ const App = (props) => {
createRoot(document.getElementById('app')!).render( );
```
+This example uses React 18+ (`createRoot`); the wrapper also supports React 16.8/17 with their rendering API. Use an element with `id="app"` in your HTML.
+
See the [example source](https://github.com/unlayer/react-email-editor/blob/master/demo/src/example/index.tsx) for a reference implementation.
### Next.js / React Server Components
@@ -145,9 +149,17 @@ Custom tools can help you add your own content blocks to the editor. Every appli
You can submit new language translations by creating a PR on this GitHub repo: https://github.com/unlayer/translations. Translations managed by [PhraseApp](https://phraseapp.com)
-## Related packages
+## Guides and related workflows
+
+[Create React templates that stay visually editable](./docs/react-elements.md) with **Unlayer Elements** and React Email Editor. The [runnable Elements demo](./demo/README.md#elements-to-visual-editor) shows the React source, loads the generated design, and lets you save, reopen, and export edits as HTML and JSON.
+
+| What you need | Package |
+| ---------------------------------------------------------------------------- | ---------------------------------------------------------------- |
+| Embed a drag-and-drop email editor in your app | `react-email-editor` |
+| Author email, web, and document templates in React | [`@unlayer/react-elements`](https://github.com/unlayer/elements) |
+| Let teammates visually edit templates written by developers or coding agents | Both, connected through `renderToJson()` and `loadDesign()` |
-- [`@unlayer/react-elements`](https://github.com/unlayer/elements): write templates in code as React components (`Email`, `Row`, `Column`, `Heading`, `Paragraph`, `Button`, …) and render them to email HTML, responsive web pages or print-ready HTML for PDFs. `renderToJson()` synchronously returns design JSON you can pass to `loadDesign`, so templates written in code (by developers, coding agents, or an LLM at runtime) stay editable in this editor.
+Elements is optional and requires React 18+. Edited design JSON does not regenerate React source. See the [integration guide](./docs/react-elements.md) for the complete workflow and constraints.
### License
diff --git a/context7.json b/context7.json
index 8098531b4..f00ad14c8 100644
--- a/context7.json
+++ b/context7.json
@@ -11,6 +11,7 @@
"Call editor methods through the ref (emailEditorRef.current?.editor), e.g. loadDesign, saveDesign and exportHtml, and only after onReady has fired.",
"Store the design JSON from saveDesign() or exportHtml() to reopen a design later with loadDesign(); send the exported html.",
"Pass Unlayer configuration through the options prop (for example projectId, displayMode, appearance, mergeTags).",
- "For templates written in code (by developers, coding agents, or an LLM), generate design JSON with renderToJson() from @unlayer/react-elements and pass it to loadDesign()."
+ "For React templates authored by developers or coding agents, follow docs/react-elements.md: use renderToJson() from @unlayer/react-elements, loadDesign() in onReady, then export edited content with editor.exportHtml().",
+ "The combined Elements/editor example requires React 18+. Persist edited design JSON to reopen it; regenerating the original React template replaces edits. JSON does not regenerate JSX."
]
}
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 000000000..ddad4d183
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,41 @@
+# React Email Editor demo
+
+Run from the repository root with Node.js 22.12+:
+
+```bash
+npm ci
+npm run build
+npm --prefix demo ci
+npm --prefix demo run dev
+```
+
+Open for the basic editor. The editor is hosted by Unlayer and requires internet access. The basic demo loads its sample after `onReady`; its save/export actions log results to the console.
+
+## Elements to visual editor
+
+Open , or follow **Try Elements** from the basic demo.
+
+The welcome email is generated with `@unlayer/react-elements` and loaded into `react-email-editor`. Edit a block, click **Save and export**, inspect/copy/download the HTML or JSON, and reload to reopen your saved design. Expand **View the React template** to see the source. Only this browser's local storage is used for saving; no backend or email delivery is provided.
+
+| File | Purpose |
+| -------------------------- | ---------------------------------------------------------------------- |
+| `src/elements/welcome.tsx` | React template and synchronous `renderToJson()` call |
+| `src/elements/index.tsx` | Editor readiness, saved-design loading, export UI, and template source |
+| `src/elements/storage.ts` | Versioned demo storage and basic saved-data checks |
+| `test/elements.test.tsx` | Readiness, export, reopening edits, and storage failures |
+
+Elements is a demo dependency, not a dependency of the editor wrapper. This route imports the built local `react-email-editor` package through `file:..`; rebuild the root package after changing wrapper code. All demo routes use the same local package.
+
+Read the [complete integration guide](../docs/react-elements.md) for installing the two packages in your own React app and handling persistence in production.
+
+## Checks
+
+From the repository root, after installation and the root build:
+
+```bash
+npm --prefix demo run build
+npm --prefix demo test
+npm --prefix demo run test:docs
+```
+
+The demo tests use the real Elements renderer and a controlled hosted-editor boundary. They do not prove hosted-editor compatibility. The documentation check compiles the README and integration guide snippets with strict TypeScript against the built wrapper. A live smoke check should additionally edit a block, export it, reload, and verify the saved edit in the real editor.
diff --git a/demo/index.tsx b/demo/index.tsx
index 8f00e0d88..9b8e53038 100644
--- a/demo/index.tsx
+++ b/demo/index.tsx
@@ -1,9 +1,11 @@
+import { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { createGlobalStyle } from 'styled-components';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Example from './src/example';
import Dashboard from './src/dashboard';
+const ElementsExample = lazy(() => import('./src/elements'));
const GlobalStyle = createGlobalStyle`
html, body {
@@ -25,6 +27,14 @@ const App = () => {
} />
} />
+ Loading the Elements example…
}>
+
+
+ }
+ />
);
diff --git a/demo/package-lock.json b/demo/package-lock.json
index ec2387cce..937e28fec 100644
--- a/demo/package-lock.json
+++ b/demo/package-lock.json
@@ -9,19 +9,101 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "@unlayer/react-elements": "0.1.22",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-email-editor": "file:..",
"react-router-dom": "^7.18.2",
"styled-components": "^6.1.19"
},
"devDependencies": {
+ "@testing-library/react": "^16.3.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.0.0",
+ "jsdom": "^29.1.1",
"typescript": "^6.0.3",
- "vite": "^7.0.0"
+ "vite": "^7.0.0",
+ "vitest": "^4.1.10"
}
},
+ "..": {
+ "version": "2.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "@unlayer/types": "^1.448.0"
+ },
+ "devDependencies": {
+ "@testing-library/react": "^16.3.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitest/coverage-v8": "^4.1.10",
+ "jsdom": "^29.1.1",
+ "prettier": "^3.9.4",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tsup": "^8.5.1",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -256,6 +338,16 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -304,6 +396,159 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz",
+ "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz",
+ "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.2",
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz",
+ "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.2",
+ "@csstools/css-calc": "^3.4.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.2",
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz",
+ "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.2"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz",
+ "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz",
+ "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
@@ -761,6 +1006,24 @@
"node": ">=18"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz",
+ "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -811,77 +1074,326 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-rc.3",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
- "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
- "dev": true,
- "license": "MIT"
+ "node_modules/@lexical/a11y": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/a11y/-/a11y-0.52.0.tgz",
+ "integrity": "sha512-FxcKnksrgeoPDxQgwHqTz81Tbv7vZnP2i09m7uU+zusNRt4g1dgbwSEq4Uee9yfLJ9qxM5P9YOGQTe0A6lAuAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/extension": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.62.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
- "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
+ "node_modules/@lexical/clipboard": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.52.0.tgz",
+ "integrity": "sha512-SwUpKUEzTfl3dZlCXkYZN4CAU6AENzzuO3AtWCgbdtxG1HBnyUtMd6MP1XFUtYh0fhMGXw+QObzp+3+h0g3WTw==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
+ "dependencies": {
+ "@lexical/extension": "0.52.0",
+ "@lexical/html": "0.52.0",
+ "@lexical/internal": "0.52.0",
+ "@lexical/list": "0.52.0",
+ "@lexical/selection": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "@types/trusted-types": "^2.0.7",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
},
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
- "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@lexical/dragon": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.52.0.tgz",
+ "integrity": "sha512-XiNrujvAoywb41GRaRYFhPc/T7jxkFMf/bR9uE8S2t4fMERUFF1xM7Mos6NQ+skdDrF3KfxnLFzLCZ+4Oc9VyA==",
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
+ "dependencies": {
+ "@lexical/extension": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
},
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
- "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@lexical/extension": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.52.0.tgz",
+ "integrity": "sha512-do7uvUxg6mJK0702XlD0Z8UMCtr1Yij4/WpJ7+GY5CL2jKVercMeeYBBEE7cWc/OXQJgzVx3ClNXC7XAHI6Fvw==",
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "@lexical/internal": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "@preact/signals-core": "^1.14.4",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
- "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/@lexical/headless": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/headless/-/headless-0.52.0.tgz",
+ "integrity": "sha512-/AvKTFBHUzGWqRuWEMGOOizlGTBrnGuu0O3g+IKj+var657uazbheDknhSq3lQGxDosOeesTM52II0kV65mEEQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
+ "dependencies": {
+ "@lexical/internal": "0.52.0",
+ "happy-dom": "^20.9.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
},
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.62.2",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
- "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/@lexical/html": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.52.0.tgz",
+ "integrity": "sha512-lRyntwgHnISsUKZHh3T+g9qU1CwTOH5hlnc1UzWE7/RXrvtnSmJTTcJu+sfFdRCiBAlkiIhKuJBCCYu99LeI7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/extension": "0.52.0",
+ "@lexical/internal": "0.52.0",
+ "@lexical/selection": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/internal": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/internal/-/internal-0.52.0.tgz",
+ "integrity": "sha512-Y7jKqeh4Irf/FwNmAqHIk3b05HpMsHv2RyWPQ8+M7FAdEbT9N2mksjc8ZUZ3tHqDMi7FNiLkjZN/DwbdekUb1g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/link": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.52.0.tgz",
+ "integrity": "sha512-9hl/SxbkK5P0q1Rn4rTnaLHetTnf4zb+zgJjQwHAkqmeS561gLq34RcoO2i1zhibe8b6xk8gyhIe2cOpPeAC7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/a11y": "0.52.0",
+ "@lexical/extension": "0.52.0",
+ "@lexical/html": "0.52.0",
+ "@lexical/internal": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/list": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.52.0.tgz",
+ "integrity": "sha512-B3+CdxWsdj6jBKtXiY7v4OSkyXBwotWyiD+75Baw0lvmjvniV33mvuMDgnPEQgEpFcGpBRmiu8M8kQdclQzXmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/extension": "0.52.0",
+ "@lexical/html": "0.52.0",
+ "@lexical/internal": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/rich-text": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.52.0.tgz",
+ "integrity": "sha512-P3Y4SM6JSCBG6uFwLpPVEZiShhfC/GCJA//lQU1ZljxPDnCb4ueVCfWSBBdw0nSejYg8KvDQsijc4Fczyjqqtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/a11y": "0.52.0",
+ "@lexical/clipboard": "0.52.0",
+ "@lexical/dragon": "0.52.0",
+ "@lexical/extension": "0.52.0",
+ "@lexical/html": "0.52.0",
+ "@lexical/selection": "0.52.0",
+ "@lexical/utils": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/selection": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.52.0.tgz",
+ "integrity": "sha512-O1hgwqUvoF/R2DxXIOJJvCqXxNM7kOWq1TBxqnbjT4TXNlDokEdpkO5EpetAWYWZj5UE2BUphfGUZAPQjvcUKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/internal": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@lexical/utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.52.0.tgz",
+ "integrity": "sha512-3Azoh0EQckRcMd6CGOAuKue/UHDvkorrheXZFu9KpxhCBNQy+ic55jl0WQZFTOv2iO3NWReAMSuOAK0pWR2JPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/internal": "0.52.0",
+ "@lexical/selection": "0.52.0",
+ "lexical": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@preact/signals-core": {
+ "version": "1.14.4",
+ "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz",
+ "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1207,6 +1719,70 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1252,6 +1828,24 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1259,6 +1853,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "26.6.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz",
+ "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.9.0"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
@@ -1279,6 +1882,69 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-mimetype": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@unlayer/exporters": {
+ "version": "1.497.0",
+ "resolved": "https://registry.npmjs.org/@unlayer/exporters/-/exporters-1.497.0.tgz",
+ "integrity": "sha512-erm4UsZcZkBTtbSkBehZ/pmhHKHDKT4kDISZ3JzzSi25ehZQpBJzEyiIQzEp135/7NDnAchbPgMask9qBo5X8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/headless": ">=0.39.0",
+ "@lexical/html": ">=0.39.0",
+ "@lexical/link": ">=0.39.0",
+ "@lexical/list": ">=0.39.0",
+ "@lexical/rich-text": ">=0.39.0",
+ "@lexical/selection": ">=0.39.0",
+ "@lexical/utils": ">=0.39.0",
+ "lexical": ">=0.39.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@unlayer/types": ">=1.497.0"
+ }
+ },
+ "node_modules/@unlayer/react-elements": {
+ "version": "0.1.22",
+ "resolved": "https://registry.npmjs.org/@unlayer/react-elements/-/react-elements-0.1.22.tgz",
+ "integrity": "sha512-WP4+6WPs9gxptcKGOQf3RsxqPhtUZBiurvxohUnVove83PLbSyK36b4b8ej9zO+lQRXcfELF766kPgh9EgRNVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@unlayer/exporters": "1.497.0",
+ "@unlayer/types": "1.497.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/@unlayer/types": {
+ "version": "1.497.0",
+ "resolved": "https://registry.npmjs.org/@unlayer/types/-/types-1.497.0.tgz",
+ "integrity": "sha512-aBxRs/v0/GfV9AFEdmdrMLMafyx0stIDBpAoSHDXV78EEzzTSgzr1OdV7St7k3j1ZLxlvNCLoFGJiwcivsOULQ==",
+ "license": "MIT"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@@ -1300,6 +1966,165 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.11",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.11",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.11",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.10.42",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
@@ -1313,6 +2138,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
+ "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/browserslist": {
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
@@ -1347,6 +2182,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-image-size": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
+ "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
@@ -1377,6 +2224,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1417,12 +2274,50 @@
"postcss-value-parser": "^4.0.2"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1441,6 +2336,32 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.387",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz",
@@ -1448,6 +2369,25 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
@@ -1500,6 +2440,26 @@
"node": ">=6"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1507,49 +2467,148 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12.0.0"
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/happy-dom": {
+ "version": "20.14.5",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz",
+ "integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=20.0.0",
+ "@types/whatwg-mimetype": "^3.0.2",
+ "@types/ws": "^8.18.1",
+ "buffer-image-size": "^0.6.4",
+ "entities": "^7.0.1",
+ "whatwg-mimetype": "^3.0.0",
+ "ws": "^8.21.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
},
"peerDependencies": {
- "picomatch": "^3 || ^4"
+ "canvas": "^3.0.0"
},
"peerDependenciesMeta": {
- "picomatch": {
+ "canvas": {
"optional": true
}
}
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
"dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
+ "license": "BlueOak-1.0.0",
"engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ "node": "20 || >=22"
}
},
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "node_modules/jsdom/node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6.9.0"
+ "node": ">=20"
}
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -1576,6 +2635,23 @@
"node": ">=6"
}
},
+ "node_modules/lexical": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.52.0.tgz",
+ "integrity": "sha512-mePejqwCDaW0GR89IoofJTTlk/EiwwlGhbs1D8qOx/Iw6ieG5D3nh7PSPLv0Csia0UC58HpuV91DilJjqeAWSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lexical/internal": "0.52.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.2"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -1586,6 +2662,34 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1622,6 +2726,53 @@
"node": ">=18"
}
},
+ "node_modules/obug": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1677,6 +2828,32 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
@@ -1698,6 +2875,18 @@
"react": "^19.2.7"
}
},
+ "node_modules/react-email-editor": {
+ "resolved": "..",
+ "link": true
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -1746,6 +2935,16 @@
"react-dom": ">=18"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
@@ -1791,6 +2990,19 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -1813,6 +3025,13 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1823,6 +3042,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/styled-components": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.3.tgz",
@@ -1865,6 +3098,30 @@
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1882,11 +3139,67 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyrainbow": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz",
+ "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.16",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz",
+ "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.16"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.16",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz",
+ "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -1896,6 +3209,22 @@
"node": ">=14.17"
}
},
+ "node_modules/undici": {
+ "version": "7.30.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
+ "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -2002,6 +3331,198 @@
}
}
},
+ "node_modules/vitest": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/demo/package.json b/demo/package.json
index 97d79cd2a..a43119435 100644
--- a/demo/package.json
+++ b/demo/package.json
@@ -8,20 +8,27 @@
"start": "vite",
"dev": "vite",
"build": "tsc --noEmit && vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:docs": "node scripts/check-docs.mjs"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.18.2",
- "styled-components": "^6.1.19"
+ "styled-components": "^6.1.19",
+ "@unlayer/react-elements": "0.1.22",
+ "react-email-editor": "file:.."
},
"devDependencies": {
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "^6.0.3",
- "vite": "^7.0.0"
+ "vite": "^7.0.0",
+ "@testing-library/react": "^16.3.2",
+ "jsdom": "^29.1.1",
+ "vitest": "^4.1.10"
},
"overrides": {
"esbuild": "^0.28.2",
diff --git a/demo/scripts/check-docs.mjs b/demo/scripts/check-docs.mjs
new file mode 100644
index 000000000..a72d8d896
--- /dev/null
+++ b/demo/scripts/check-docs.mjs
@@ -0,0 +1,49 @@
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
+import { join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+
+const demo = fileURLToPath(new URL('..', import.meta.url));
+const root = resolve(demo, '..');
+const temporary = await mkdtemp(join(demo, 'node_modules', '.docs-check-'));
+try {
+ const paths = [];
+ for (const [name, source] of [
+ ['readme', 'README.md'],
+ ['elements', 'docs/react-elements.md'],
+ ]) {
+ const markdown = await readFile(join(root, source), 'utf8');
+ const blocks = [...markdown.matchAll(/```tsx\n([\s\S]*?)```/g)];
+ if (blocks.length !== 1)
+ throw new Error(`Expected one complete TSX example in ${source}`);
+ const path = join(temporary, `${name}.tsx`);
+ await writeFile(path, blocks[0][1]);
+ paths.push(path);
+ }
+ const result = spawnSync(
+ process.execPath,
+ [
+ join(demo, 'node_modules/typescript/bin/tsc'),
+ '--ignoreConfig',
+ '--noEmit',
+ '--strict',
+ '--skipLibCheck',
+ '--jsx',
+ 'react-jsx',
+ '--target',
+ 'ES2020',
+ '--module',
+ 'ESNext',
+ '--moduleResolution',
+ 'bundler',
+ '--verbatimModuleSyntax',
+ ...paths,
+ ],
+ { stdio: 'inherit' }
+ );
+ if (result.error) throw result.error;
+ if (result.status !== 0) throw new Error('Documentation typecheck failed');
+ console.log('Both documentation examples pass strict TypeScript checks.');
+} finally {
+ await rm(temporary, { recursive: true, force: true });
+}
diff --git a/demo/src/dashboard/DesignEdit.tsx b/demo/src/dashboard/DesignEdit.tsx
index 563cadc0c..f3b9fb102 100644
--- a/demo/src/dashboard/DesignEdit.tsx
+++ b/demo/src/dashboard/DesignEdit.tsx
@@ -2,7 +2,7 @@ import React, { useRef } from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
-import EmailEditor, { EditorRef } from '../../../src'; // use react-email-editor instead
+import EmailEditor, { EditorRef } from 'react-email-editor';
const Container = styled.div`
display: flex;
@@ -88,10 +88,10 @@ const DesignEdit = () => {
diff --git a/demo/src/elements/index.tsx b/demo/src/elements/index.tsx
new file mode 100644
index 000000000..2e529ce12
--- /dev/null
+++ b/demo/src/elements/index.tsx
@@ -0,0 +1,188 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Link } from 'react-router-dom';
+import EmailEditor, {
+ type EditorRef,
+ type EmailEditorProps,
+} from 'react-email-editor';
+import { createWelcomeDesign } from './welcome';
+import { readSavedDesign, saveDesign, type EmailDesign } from './storage';
+import welcomeSource from './welcome.tsx?raw';
+import './style.css';
+
+type ExportedEmail = { design: EmailDesign; html: string };
+
+export default function ElementsExample() {
+ const editorRef = useRef(null);
+ const [ready, setReady] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [status, setStatus] = useState('Loading the editor…');
+ const [error, setError] = useState('');
+ const [output, setOutput] = useState(null);
+ const [format, setFormat] = useState<'html' | 'json'>('html');
+
+ useEffect(() => {
+ if (ready) return;
+ const timeout = window.setTimeout(() => {
+ setStatus(
+ 'The editor is still loading. Check your connection or browser blockers, then reload.'
+ );
+ }, 30_000);
+ return () => window.clearTimeout(timeout);
+ }, [ready]);
+
+ const onReady = useCallback>(
+ (editor) => {
+ let message = 'React template loaded. Edit it, then save and export.';
+ editor.addEventListener('design:loaded', () => {
+ setReady(true);
+ setStatus(message);
+ });
+ let saved: EmailDesign | null = null;
+ try {
+ saved = readSavedDesign(window.localStorage);
+ if (saved)
+ message =
+ 'Your saved design was reopened. Edit it, then save and export.';
+ } catch {
+ setError(
+ 'Could not read a saved design. The React template was loaded instead.'
+ );
+ }
+ editor.loadDesign(saved ?? createWelcomeDesign());
+ },
+ []
+ );
+
+ const exportEmail = () => {
+ if (!ready || busy || !editorRef.current?.editor) return;
+ setBusy(true);
+ setError('');
+ try {
+ editorRef.current.editor.exportHtml(({ design, html }) => {
+ setOutput({ design, html });
+ try {
+ saveDesign(window.localStorage, design);
+ setStatus(
+ 'Saved in this browser. Reload this page to reopen your edits.'
+ );
+ } catch {
+ setError(
+ 'Export succeeded, but browser storage is unavailable. Download the JSON to keep your edits.'
+ );
+ } finally {
+ setBusy(false);
+ }
+ });
+ } catch {
+ setError('Could not export the design. Please try again.');
+ setBusy(false);
+ }
+ };
+
+ const contents = output
+ ? format === 'html'
+ ? output.html
+ : JSON.stringify(output.design, null, 2)
+ : '';
+
+ const copyOutput = async () => {
+ try {
+ await navigator.clipboard.writeText(contents);
+ setStatus(`${format.toUpperCase()} copied.`);
+ } catch {
+ setError(
+ 'Could not copy. Select the output below or download it instead.'
+ );
+ }
+ };
+
+ const downloadOutput = () => {
+ const url = URL.createObjectURL(
+ new Blob([contents], {
+ type: format === 'html' ? 'text/html' : 'application/json',
+ })
+ );
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `welcome.${format}`;
+ link.click();
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
+ };
+
+ return (
+
+
+ ← Basic editor
+ React templates, visually editable
+
+ This welcome email was written with{' '}
+ Unlayer Elements .
+ Edit it below, then save and export it.
+
+
+ Demo saves stay in this browser. Your application would store design
+ JSON in its own backend.
+
+
+ {busy ? 'Exporting…' : 'Save and export'}
+
+ {status}
+ {error && {error}
}
+
+
+
+
+ Exported email
+ Save and export to inspect the latest visual edits.
+
+
+ Output{' '}
+
+ setFormat(event.target.value as 'html' | 'json')
+ }
+ >
+ HTML
+ Design JSON
+
+
+
+ Copy
+
+
+ Download
+
+
+
+
+
+
+ View the React template
+
+ {welcomeSource}
+
+
+
+
+ );
+}
diff --git a/demo/src/elements/storage.ts b/demo/src/elements/storage.ts
new file mode 100644
index 000000000..be7992bf1
--- /dev/null
+++ b/demo/src/elements/storage.ts
@@ -0,0 +1,29 @@
+import type { EditorRef } from 'react-email-editor';
+
+export type Editor = NonNullable;
+export type EmailDesign = Parameters[0];
+export const STORAGE_KEY = 'react-email-editor:elements-design:v1';
+
+// Sanity-check this demo's saved exports, not arbitrary imported designs.
+export function readSavedDesign(storage: Storage): EmailDesign | null {
+ const saved = storage.getItem(STORAGE_KEY);
+ if (!saved) return null;
+ const value = JSON.parse(saved);
+ const design = value?.design;
+ if (
+ value?.version !== 1 ||
+ typeof design?.schemaVersion !== 'number' ||
+ !design.counters ||
+ typeof design.counters !== 'object' ||
+ !Array.isArray(design.body?.rows) ||
+ !design.body.values ||
+ typeof design.body.values !== 'object'
+ ) {
+ throw new Error('The saved design has an unsupported format.');
+ }
+ return design;
+}
+
+export function saveDesign(storage: Storage, design: EmailDesign): void {
+ storage.setItem(STORAGE_KEY, JSON.stringify({ version: 1, design }));
+}
diff --git a/demo/src/elements/style.css b/demo/src/elements/style.css
new file mode 100644
index 000000000..9642b0a5d
--- /dev/null
+++ b/demo/src/elements/style.css
@@ -0,0 +1,114 @@
+.elements-demo {
+ padding: 24px;
+ background: #f4f6f8;
+ color: #172b3a;
+ min-height: 100vh;
+ box-sizing: border-box;
+}
+.elements-demo header,
+.elements-demo footer,
+.elements-demo details {
+ max-width: 1500px;
+ margin: 0 auto 20px;
+}
+.elements-demo h1 {
+ margin: 20px 0 12px;
+ font-size: 30px;
+}
+.elements-demo h2 {
+ font-size: 18px;
+}
+.elements-demo a {
+ color: #075b85;
+}
+.elements-demo p {
+ line-height: 1.5;
+}
+.elements-demo .hint {
+ color: #536470;
+ font-size: 14px;
+}
+.elements-demo button {
+ background: #0879a1;
+ color: white;
+ border: 0;
+ border-radius: 5px;
+ padding: 10px 14px;
+ cursor: pointer;
+}
+.elements-demo button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.elements-demo [role='alert'] {
+ color: #a1261b;
+}
+.elements-demo .workspace {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 340px;
+ gap: 20px;
+ max-width: 1500px;
+ margin: 0 auto 24px;
+}
+.elements-demo .editor-panel,
+.elements-demo .output-panel {
+ background: white;
+ border: 1px solid #d5dee5;
+ border-radius: 6px;
+ overflow: hidden;
+}
+.elements-demo .output-panel {
+ padding: 16px;
+}
+.elements-demo .output-actions {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin: 16px 0;
+}
+.elements-demo select {
+ padding: 8px 4px;
+}
+.elements-demo textarea {
+ box-sizing: border-box;
+ width: 100%;
+ height: 470px;
+ resize: vertical;
+ border: 1px solid #bac8d2;
+ border-radius: 4px;
+ padding: 12px;
+ font: 12px/1.5 monospace;
+}
+.elements-demo summary {
+ cursor: pointer;
+ padding: 12px 0;
+ font-weight: bold;
+}
+.elements-demo pre {
+ padding: 20px;
+ overflow: auto;
+ background: #172b3a;
+ color: #eef5fa;
+ border-radius: 6px;
+ font: 13px/1.6 monospace;
+}
+.elements-demo footer {
+ font-size: 14px;
+}
+@media (max-width: 1400px) {
+ .elements-demo .workspace {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .elements-demo textarea {
+ height: 220px;
+ }
+}
+@media (max-width: 600px) {
+ .elements-demo {
+ padding: 12px;
+ }
+ .elements-demo h1 {
+ font-size: 24px;
+ }
+}
diff --git a/demo/src/elements/welcome.tsx b/demo/src/elements/welcome.tsx
new file mode 100644
index 000000000..a12a77d7c
--- /dev/null
+++ b/demo/src/elements/welcome.tsx
@@ -0,0 +1,40 @@
+import {
+ Email,
+ Row,
+ Column,
+ Heading,
+ Paragraph,
+ Button,
+ renderToJson,
+} from '@unlayer/react-elements';
+import type { EmailDesign } from './storage';
+
+export function createWelcomeDesign() {
+ // Elements 0.1.22 types values broadly; the editor expects its stricter design type.
+ // This assertion applies only to the supported Elements template below.
+ return renderToJson(
+
+
+
+
+ Welcome to Acme
+
+
+
+ Open your workspace
+
+
+
+
+
+ ) as EmailDesign;
+}
diff --git a/demo/src/example/index.tsx b/demo/src/example/index.tsx
index 4fb838f24..bab735b74 100644
--- a/demo/src/example/index.tsx
+++ b/demo/src/example/index.tsx
@@ -1,8 +1,9 @@
import React, { useRef, useState } from 'react';
import styled from 'styled-components';
+import { Link } from 'react-router-dom';
import packageJson from '../../../package.json';
-import EmailEditor, { EditorRef, EmailEditorProps } from '../../../src'; // use react-email-editor instead
+import EmailEditor, { EditorRef, EmailEditorProps } from 'react-email-editor';
import type { JSONTemplate } from '@unlayer/types';
import _sample from './sample.json';
@@ -29,6 +30,17 @@ const Bar = styled.div`
text-align: left;
}
+ a {
+ align-self: center;
+ color: #000;
+ white-space: nowrap;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: wait;
+ }
+
button {
flex: 1;
padding: 10px;
@@ -46,6 +58,7 @@ const Bar = styled.div`
const Example = () => {
const emailEditorRef = useRef(null);
const [preview, setPreview] = useState(false);
+ const [ready, setReady] = useState(false);
const saveDesign = () => {
const unlayer = emailEditorRef.current?.editor;
@@ -81,28 +94,42 @@ const Example = () => {
const onDesignLoad = (data: { design: JSONTemplate<'email'> }) => {
console.log('onDesignLoad', data);
+ setReady(true);
};
const onLoad: EmailEditorProps['onLoad'] = (unlayer) => {
console.log('onLoad', unlayer);
unlayer.addEventListener('design:loaded', onDesignLoad);
- unlayer.loadDesign(sample);
};
const onReady: EmailEditorProps['onReady'] = (unlayer) => {
- console.log('onReady', unlayer);
+ unlayer.loadDesign(sample);
};
return (
- React Email Editor v{packageJson.version} (Demo) — (GitHub )
-
-
+
+ React Email Editor v{packageJson.version} (Demo) — (
+
+ GitHub
+
+ )
+
+
+ Try Elements
+
{preview ? 'Hide' : 'Show'} Preview
- Save Design
- Export HTML
+
+ Save Design
+
+
+ Export HTML
+
@@ -111,10 +138,10 @@ const Example = () => {
onLoad={onLoad}
onReady={onReady}
options={{
- version: "latest",
+ version: 'latest',
appearance: {
- theme: "modern_light"
- }
+ theme: 'modern_light',
+ },
}}
/>
diff --git a/demo/test/elements.test.tsx b/demo/test/elements.test.tsx
new file mode 100644
index 000000000..9439868dc
--- /dev/null
+++ b/demo/test/elements.test.tsx
@@ -0,0 +1,204 @@
+import React from 'react';
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+} from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { EmailEditorProps } from 'react-email-editor';
+import ElementsExample from '../src/elements';
+import { createWelcomeDesign } from '../src/elements/welcome';
+import { STORAGE_KEY } from '../src/elements/storage';
+
+const harness = vi.hoisted(() => ({
+ props: null as EmailEditorProps | null,
+ listeners: {} as Record void>,
+ loadDesign: vi.fn(),
+ exportHtml: vi.fn(),
+}));
+
+// UI/persistence tests use the real Elements renderer and a controlled hosted-editor boundary.
+vi.mock('react-email-editor', async () => {
+ const React = await import('react');
+ return {
+ default: React.forwardRef((props: EmailEditorProps, ref) => {
+ harness.props = props;
+ React.useImperativeHandle(ref, () => ({ editor: harness }));
+ return Hosted editor
;
+ }),
+ };
+});
+
+function mount() {
+ return render(
+
+
+
+ );
+}
+
+function signalReady() {
+ act(() => {
+ harness.props?.onReady?.({
+ ...harness,
+ addEventListener: (name: string, callback: () => void) => {
+ harness.listeners[name] = callback;
+ },
+ } as unknown as Parameters>[0]);
+ });
+}
+
+beforeEach(() => {
+ window.localStorage.clear();
+ harness.listeners = {};
+ harness.loadDesign.mockReset();
+ harness.exportHtml.mockReset();
+ harness.exportHtml.mockImplementation((callback) =>
+ callback({
+ design: createWelcomeDesign(),
+ html: 'Welcome to Acme',
+ })
+ );
+});
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+describe('Elements integration', () => {
+ it('generates a complete design and gates export on the design-loaded event', () => {
+ mount();
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(true);
+ expect(harness.loadDesign).not.toHaveBeenCalled();
+ signalReady();
+ const design = harness.loadDesign.mock.calls[0][0];
+ expect(design.schemaVersion).toBeGreaterThan(0);
+ expect(design.body.rows[0].columns[0].contents).toHaveLength(4);
+ expect(JSON.stringify(design)).toContain('Welcome to Acme');
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(true);
+ act(() => harness.listeners['design:loaded']());
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(false);
+ });
+
+ it('saves the edited design and reopens it instead of regenerating the template', () => {
+ const first = mount();
+ signalReady();
+ act(() => harness.listeners['design:loaded']());
+ const edited = JSON.parse(
+ JSON.stringify(createWelcomeDesign()).replace(
+ 'Welcome to Acme',
+ 'Welcome back, Ada'
+ )
+ );
+ harness.exportHtml.mockImplementation((callback) =>
+ callback({
+ design: edited,
+ html: 'Welcome back, Ada',
+ })
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ expect(
+ (
+ screen.getByRole('textbox', {
+ name: 'Exported output',
+ }) as HTMLTextAreaElement
+ ).value
+ ).toContain('Welcome back, Ada');
+ expect(
+ JSON.parse(window.localStorage.getItem(STORAGE_KEY)!).design
+ ).toEqual(edited);
+ fireEvent.change(screen.getByRole('combobox', { name: 'Output' }), {
+ target: { value: 'json' },
+ });
+ expect(
+ JSON.parse(
+ (
+ screen.getByRole('textbox', {
+ name: 'Exported output',
+ }) as HTMLTextAreaElement
+ ).value
+ )
+ ).toEqual(edited);
+ first.unmount();
+ mount();
+ signalReady();
+ expect(harness.loadDesign).toHaveBeenLastCalledWith(edited);
+ });
+
+ it('recovers from malformed saved data with a visible warning', () => {
+ window.localStorage.setItem(STORAGE_KEY, '{broken');
+ mount();
+ signalReady();
+ expect(screen.getByRole('alert').textContent).toContain('Could not read');
+ expect(JSON.stringify(harness.loadDesign.mock.calls[0][0])).toContain(
+ 'Welcome to Acme'
+ );
+ });
+
+ it('allows retrying after an export error', () => {
+ mount();
+ signalReady();
+ act(() => harness.listeners['design:loaded']());
+ harness.exportHtml.mockImplementationOnce(() => {
+ throw new Error('Export failed');
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ expect(screen.getByRole('alert').textContent).toContain('Could not export');
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(false);
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ expect(screen.queryByRole('alert')).toBeNull();
+ expect(
+ (screen.getByRole('button', { name: 'Download' }) as HTMLButtonElement)
+ .disabled
+ ).toBe(false);
+ });
+
+ it('keeps exports available when storage writes fail', () => {
+ mount();
+ signalReady();
+ act(() => harness.listeners['design:loaded']());
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
+ throw new Error('Quota exceeded');
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ expect(screen.getByRole('alert').textContent).toContain('Export succeeded');
+ expect(
+ (screen.getByRole('button', { name: 'Download' }) as HTMLButtonElement)
+ .disabled
+ ).toBe(false);
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(false);
+ });
+});
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
index 6a87bc22f..e1eb8e104 100644
--- a/demo/tsconfig.json
+++ b/demo/tsconfig.json
@@ -15,5 +15,12 @@
"noEmit": true,
"useDefineForClassFields": true
},
- "include": ["index.tsx", "src", "vite.config.ts"]
+ "include": [
+ "index.tsx",
+ "src",
+ "vite.config.ts",
+ "vite-env.d.ts",
+ "vitest.config.ts",
+ "test"
+ ]
}
diff --git a/demo/vite-env.d.ts b/demo/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/demo/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/demo/vite.config.ts b/demo/vite.config.ts
index 13a29b82b..8b2cdac33 100644
--- a/demo/vite.config.ts
+++ b/demo/vite.config.ts
@@ -4,8 +4,8 @@ import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
- // The demo imports the library source from ../src, which resolves react
- // from the root node_modules — dedupe so only one copy is bundled.
+ // The local wrapper dependency resolves React from the root node_modules.
+ // Dedupe so the demo and wrapper share one copy.
dedupe: ['react', 'react-dom', 'styled-components'],
},
server: {
diff --git a/demo/vitest.config.ts b/demo/vitest.config.ts
new file mode 100644
index 000000000..0464ce293
--- /dev/null
+++ b/demo/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: { dedupe: ['react', 'react-dom'] },
+ test: { environment: 'jsdom', include: ['test/*.test.tsx'] },
+});
diff --git a/docs/react-elements.md b/docs/react-elements.md
new file mode 100644
index 000000000..5d3fdd082
--- /dev/null
+++ b/docs/react-elements.md
@@ -0,0 +1,121 @@
+# Create React email templates that stay visually editable
+
+Use **Unlayer Elements** (`@unlayer/react-elements`) to write templates in React, then use **React Email Editor** (`react-email-editor`) to let teammates edit them visually. This also gives coding agents a supported component structure for generating editable email templates.
+
+Elements is optional. If users build everything visually, use React Email Editor alone. If your application only needs server-rendered email HTML, Elements can run without an embedded editor.
+
+## Install
+
+In a React 18+ application:
+
+```bash
+npm install react-email-editor @unlayer/react-elements react react-dom
+```
+
+The combined example requires React 18+ because Elements does; the editor wrapper alone supports React 16.8+. The checked-in demo uses React 19 and Elements 0.1.22. The visual editor runs in the browser and needs access to Unlayer's hosted editor. Local Elements rendering does not need an Unlayer account.
+
+## Generate design JSON and load it after readiness
+
+Save this complete component as `src/App.tsx` in your React TypeScript app. It generates an initial template and exposes the edited HTML and JSON:
+
+```tsx
+import { useRef, useState } from 'react';
+import EmailEditor, {
+ type EditorRef,
+ type EmailEditorProps,
+} from 'react-email-editor';
+import {
+ Email,
+ Row,
+ Column,
+ Heading,
+ Paragraph,
+ renderToJson,
+} from '@unlayer/react-elements';
+
+type EmailDesign = Parameters<
+ NonNullable['loadDesign']
+>[0];
+
+function createDesign() {
+ return renderToJson(
+
+
+
+ Welcome to Acme
+
+
+
+
+ ) as EmailDesign; // Bridge Elements 0.1.22's broad values to the editor design type.
+}
+
+export default function App() {
+ const ref = useRef(null);
+ const [ready, setReady] = useState(false);
+ const [html, setHtml] = useState('');
+ const [json, setJson] = useState('');
+
+ const onReady: EmailEditorProps['onReady'] = (editor) => {
+ editor.addEventListener('design:loaded', () => setReady(true));
+ editor.loadDesign(createDesign());
+ };
+
+ const exportEmail = () => {
+ if (!ready) return;
+ ref.current?.editor?.exportHtml(({ design, html }) => {
+ setHtml(html);
+ setJson(JSON.stringify(design, null, 2));
+ // Store design in your backend and pass it to loadDesign() when reopening.
+ });
+ };
+
+ return (
+
+
+ Export edited email
+
+
+
+
+
+ );
+}
+```
+
+Expected: an editable welcome email. After changing its text and clicking **Export edited email**, the HTML and JSON text areas contain the latest visual edits. `renderToJson()` returns a design object synchronously; `loadDesign()` accepts that object; `exportHtml()` returns both edited HTML and design JSON through its callback.
+
+Elements 0.1.22 declares design values more broadly than the editor's types. The assertion above bridges those declarations for this supported template; it does not validate arbitrary JSON or HTML.
+
+For Next.js App Router, put `'use client';` at the top of this component. For server-generated templates, call Elements' `renderToJson()` on the server and pass the serializable design object to the client component instead of generating it there.
+
+## Run the complete save-and-reopen demo
+
+From this repository's root, with Node.js 22.12+:
+
+```bash
+npm ci
+npm run build
+npm --prefix demo ci
+npm --prefix demo run dev
+```
+
+Open . The [demo README](../demo/README.md#elements-to-visual-editor) describes the files and validation commands. The demo consumes the built local wrapper and the published Elements package.
+
+1. Edit the welcome heading or paragraph in the visual editor.
+2. Click **Save and export**. Inspect HTML or Design JSON, then copy or download it.
+3. Reload the page. Your saved design is loaded instead of regenerating the React template.
+4. Expand **View the React template** to compare the source with your editable result.
+
+The demo stores the latest design in this browser's local storage. It is not a production persistence service or an autosave implementation. If storage is unavailable, the output remains downloadable and the page shows a warning. For production, save the design with authorization and versioning in your backend, handle save failures, and load the saved JSON in `onReady`. Use `saveDesign(callback)` when you need only JSON, or `exportHtml(callback)` when you also need HTML. Debounce `design:updated` events if implementing autosave.
+
+## Constraints
+
+- Use the structure `Email > Row > Column > content`. `renderToJson()` walks supported Elements nodes; it does not render arbitrary React component trees or run hooks. Fetch data before generating the design.
+- Load the initial design after `onReady` and wait for `design:loaded` before enabling export. Changing editor configuration can recreate the editor; preserve unsaved work before doing so.
+- Once someone edits the template, persist their edited design. Regenerating from the original React tree would replace those changes. JSON preserves design content and settings, not JSX, application logic, or event handlers.
+- Use `editor.exportHtml()` for HTML from an edited design. Elements' `renderToHtml()` takes React elements, not saved JSON. Your email provider sends the exported HTML; keep any plain-text alternative in sync.
+- Escape dynamic values interpolated into raw `html` props and validate links. Generated code should go through your normal review/build process before execution.
+- Hosted editor project settings, custom tools, and feature availability are separate from the wrapper's MIT license. Use matching custom-tool registrations and the appropriate display mode. See [editor installation](https://docs.unlayer.com/builder/installation).
+
+For component APIs, provider integrations, and sharing templates across email, web, and PDF, continue with the [Elements documentation](https://github.com/unlayer/elements/tree/main/packages/react/docs). Keep the editor integration here and the rendering/provider recipes in Elements.
From 66f13b5b941d448a4e264d2c6aeef7c51d78ad69 Mon Sep 17 00:00:00 2001
From: Adeel Raza
Date: Sun, 4 Oct 2026 10:34:06 +0400
Subject: [PATCH 2/5] docs: keep the Elements example secondary and clarify
development
---
CONTRIBUTING.md | 24 ++++++++++++++++++++----
README.md | 15 ++++-----------
context7.json | 2 +-
demo/README.md | 5 ++---
demo/src/elements/welcome.tsx | 4 +---
docs/react-elements.md | 5 +++--
6 files changed, 31 insertions(+), 24 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de2bbe201..1113dca77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,15 +1,31 @@
## Prerequisites
-[Node.js](http://nodejs.org/) >= v10 must be installed.
+[Node.js](https://nodejs.org/) 22, matching `.nvmrc`, must be installed.
## Installation
-- Running `npm install` in the components's root directory will install everything you need for development.
-- Running `npm install` in the `demo` directory will install everything you need to run a demo app locally.
+From the repository root:
+
+```bash
+npm install
+npm --prefix demo install
+```
+
+The root install builds the wrapper through its `prepare` script.
## Demo Development Server
-- `npm start` from `demo` directory will run the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
+The demo consumes the built wrapper through `file:..`. Run these commands in two terminals, both from the repository root:
+
+```bash
+npm start
+```
+
+```bash
+npm --prefix demo run dev
+```
+
+`npm start` runs `tsup --watch` to rebuild wrapper changes. The demo server reloads those builds at [http://localhost:3000](http://localhost:3000); running only the demo server will not pick up wrapper source edits.
## Running Tests
diff --git a/README.md b/README.md
index 4dcd5462b..940dbc41b 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,8 @@ See the [CHANGELOG](CHANGELOG.md) for the full list.
Require the EmailEditor component and render it with JSX:
+
+
```tsx
import { useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
@@ -77,7 +79,6 @@ const App = () => {
const onReady: EmailEditorProps['onReady'] = (editor) => {
setReady(true);
// editor is now ready for calls such as saveDesign and loadDesign.
- console.log('Editor ready', editor);
// Load an existing design here: editor.loadDesign(savedDesign).
// Obtain design JSON from editor.saveDesign(callback), editor.exportHtml(callback),
// or @unlayer/react-elements' renderToJson(). loadDesign accepts JSON, not a callback.
@@ -97,7 +98,7 @@ const App = () => {
createRoot(document.getElementById('app')!).render( );
```
-This example uses React 18+ (`createRoot`); the wrapper also supports React 16.8/17 with their rendering API. Use an element with `id="app"` in your HTML.
+This example requires React 18+ for `createRoot` and uses the automatic JSX transform. With React 16.8/17, use `ReactDOM.render`; also add `import React` when using the classic JSX transform. Use an element with `id="app"` in your HTML.
See the [example source](https://github.com/unlayer/react-email-editor/blob/master/demo/src/example/index.tsx) for a reference implementation.
@@ -151,15 +152,7 @@ You can submit new language translations by creating a PR on this GitHub repo: h
## Guides and related workflows
-[Create React templates that stay visually editable](./docs/react-elements.md) with **Unlayer Elements** and React Email Editor. The [runnable Elements demo](./demo/README.md#elements-to-visual-editor) shows the React source, loads the generated design, and lets you save, reopen, and export edits as HTML and JSON.
-
-| What you need | Package |
-| ---------------------------------------------------------------------------- | ---------------------------------------------------------------- |
-| Embed a drag-and-drop email editor in your app | `react-email-editor` |
-| Author email, web, and document templates in React | [`@unlayer/react-elements`](https://github.com/unlayer/elements) |
-| Let teammates visually edit templates written by developers or coding agents | Both, connected through `renderToJson()` and `loadDesign()` |
-
-Elements is optional and requires React 18+. Edited design JSON does not regenerate React source. See the [integration guide](./docs/react-elements.md) for the complete workflow and constraints.
+Writing templates in React? See the [Elements example](./docs/react-elements.md): generate design JSON with `@unlayer/react-elements` and edit it here.
### License
diff --git a/context7.json b/context7.json
index f00ad14c8..456129817 100644
--- a/context7.json
+++ b/context7.json
@@ -11,7 +11,7 @@
"Call editor methods through the ref (emailEditorRef.current?.editor), e.g. loadDesign, saveDesign and exportHtml, and only after onReady has fired.",
"Store the design JSON from saveDesign() or exportHtml() to reopen a design later with loadDesign(); send the exported html.",
"Pass Unlayer configuration through the options prop (for example projectId, displayMode, appearance, mergeTags).",
- "For React templates authored by developers or coding agents, follow docs/react-elements.md: use renderToJson() from @unlayer/react-elements, loadDesign() in onReady, then export edited content with editor.exportHtml().",
+ "Use renderToJson() from @unlayer/react-elements; loadDesign() in onReady; wait for design:loaded; exportHtml(). Guide: https://github.com/unlayer/react-email-editor/blob/master/docs/react-elements.md",
"The combined Elements/editor example requires React 18+. Persist edited design JSON to reopen it; regenerating the original React template replaces edits. JSON does not regenerate JSX."
]
}
diff --git a/demo/README.md b/demo/README.md
index ddad4d183..dcb514dfd 100644
--- a/demo/README.md
+++ b/demo/README.md
@@ -4,7 +4,6 @@ Run from the repository root with Node.js 22.12+:
```bash
npm ci
-npm run build
npm --prefix demo ci
npm --prefix demo run dev
```
@@ -24,7 +23,7 @@ The welcome email is generated with `@unlayer/react-elements` and loaded into `r
| `src/elements/storage.ts` | Versioned demo storage and basic saved-data checks |
| `test/elements.test.tsx` | Readiness, export, reopening edits, and storage failures |
-Elements is a demo dependency, not a dependency of the editor wrapper. This route imports the built local `react-email-editor` package through `file:..`; rebuild the root package after changing wrapper code. All demo routes use the same local package.
+Elements is a demo dependency, not a dependency of the editor wrapper. This route imports the built local `react-email-editor` package through `file:..`; run the root `npm start` watcher alongside the demo server when editing wrapper code (see [contributing](../CONTRIBUTING.md#demo-development-server)). All demo routes use the same local package.
Read the [complete integration guide](../docs/react-elements.md) for installing the two packages in your own React app and handling persistence in production.
@@ -38,4 +37,4 @@ npm --prefix demo test
npm --prefix demo run test:docs
```
-The demo tests use the real Elements renderer and a controlled hosted-editor boundary. They do not prove hosted-editor compatibility. The documentation check compiles the README and integration guide snippets with strict TypeScript against the built wrapper. A live smoke check should additionally edit a block, export it, reload, and verify the saved edit in the real editor.
+The demo tests use the real Elements renderer and a controlled hosted-editor boundary. They do not prove hosted-editor compatibility. The documentation check compiles complete TSX examples marked with `` in the README and integration guide. It uses strict TypeScript against the built wrapper and reports errors at their Markdown lines; unmarked explanatory fragments are ignored. A live smoke check should additionally edit a block, export it, reload, and verify the saved edit in the real editor.
diff --git a/demo/src/elements/welcome.tsx b/demo/src/elements/welcome.tsx
index a12a77d7c..1f353d2fb 100644
--- a/demo/src/elements/welcome.tsx
+++ b/demo/src/elements/welcome.tsx
@@ -10,8 +10,6 @@ import {
import type { EmailDesign } from './storage';
export function createWelcomeDesign() {
- // Elements 0.1.22 types values broadly; the editor expects its stricter design type.
- // This assertion applies only to the supported Elements template below.
return renderToJson(
- ) as EmailDesign;
+ ) as EmailDesign; // Bridge Elements 0.1.22 design types to the editor.
}
diff --git a/docs/react-elements.md b/docs/react-elements.md
index 5d3fdd082..9511b74ee 100644
--- a/docs/react-elements.md
+++ b/docs/react-elements.md
@@ -18,6 +18,8 @@ The combined example requires React 18+ because Elements does; the editor wrappe
Save this complete component as `src/App.tsx` in your React TypeScript app. It generates an initial template and exposes the edited HTML and JSON:
+
+
```tsx
import { useRef, useState } from 'react';
import EmailEditor, {
@@ -47,7 +49,7 @@ function createDesign() {
- ) as EmailDesign; // Bridge Elements 0.1.22's broad values to the editor design type.
+ ) as EmailDesign; // Bridge Elements 0.1.22 design types to the editor.
}
export default function App() {
@@ -95,7 +97,6 @@ From this repository's root, with Node.js 22.12+:
```bash
npm ci
-npm run build
npm --prefix demo ci
npm --prefix demo run dev
```
From 6d45db29078ecd096c8fc2deac116d8ed7bc9fa3 Mon Sep 17 00:00:00 2001
From: Adeel Raza
Date: Sun, 4 Oct 2026 10:34:13 +0400
Subject: [PATCH 3/5] fix(demo): recover from export timeouts and corrupt
storage
---
demo/src/elements/index.tsx | 27 +++++++-
demo/src/elements/storage.ts | 36 +++++++----
demo/test/elements.test.tsx | 119 ++++++++++++++++++++++++++++++++++-
demo/test/setup.ts | 8 +++
demo/vitest.config.ts | 6 +-
5 files changed, 179 insertions(+), 17 deletions(-)
create mode 100644 demo/test/setup.ts
diff --git a/demo/src/elements/index.tsx b/demo/src/elements/index.tsx
index 2e529ce12..ece5103ee 100644
--- a/demo/src/elements/index.tsx
+++ b/demo/src/elements/index.tsx
@@ -13,6 +13,7 @@ type ExportedEmail = { design: EmailDesign; html: string };
export default function ElementsExample() {
const editorRef = useRef(null);
+ const pendingExport = useRef<(() => boolean) | null>(null);
const [ready, setReady] = useState(false);
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState('Loading the editor…');
@@ -30,6 +31,13 @@ export default function ElementsExample() {
return () => window.clearTimeout(timeout);
}, [ready]);
+ useEffect(
+ () => () => {
+ pendingExport.current?.();
+ },
+ []
+ );
+
const onReady = useCallback>(
(editor) => {
let message = 'React template loaded. Edit it, then save and export.';
@@ -54,11 +62,27 @@ export default function ElementsExample() {
);
const exportEmail = () => {
- if (!ready || busy || !editorRef.current?.editor) return;
+ if (!ready || pendingExport.current || !editorRef.current?.editor) return;
setBusy(true);
setError('');
+ let active = true;
+ // Finish once; ignore callbacks after a timeout, retry, or unmount.
+ const finish = () => {
+ if (!active) return false;
+ active = false;
+ window.clearTimeout(timeout);
+ pendingExport.current = null;
+ return true;
+ };
+ const timeout = window.setTimeout(() => {
+ if (!finish()) return;
+ setBusy(false);
+ setError('Export timed out. Please try again.');
+ }, 30_000);
+ pendingExport.current = finish;
try {
editorRef.current.editor.exportHtml(({ design, html }) => {
+ if (!finish()) return;
setOutput({ design, html });
try {
saveDesign(window.localStorage, design);
@@ -74,6 +98,7 @@ export default function ElementsExample() {
}
});
} catch {
+ if (!finish()) return;
setError('Could not export the design. Please try again.');
setBusy(false);
}
diff --git a/demo/src/elements/storage.ts b/demo/src/elements/storage.ts
index be7992bf1..610525a73 100644
--- a/demo/src/elements/storage.ts
+++ b/demo/src/elements/storage.ts
@@ -8,20 +8,30 @@ export const STORAGE_KEY = 'react-email-editor:elements-design:v1';
export function readSavedDesign(storage: Storage): EmailDesign | null {
const saved = storage.getItem(STORAGE_KEY);
if (!saved) return null;
- const value = JSON.parse(saved);
- const design = value?.design;
- if (
- value?.version !== 1 ||
- typeof design?.schemaVersion !== 'number' ||
- !design.counters ||
- typeof design.counters !== 'object' ||
- !Array.isArray(design.body?.rows) ||
- !design.body.values ||
- typeof design.body.values !== 'object'
- ) {
- throw new Error('The saved design has an unsupported format.');
+ try {
+ const value = JSON.parse(saved);
+ const design = value?.design;
+ if (
+ value?.version !== 1 ||
+ typeof design?.schemaVersion !== 'number' ||
+ !design.counters ||
+ typeof design.counters !== 'object' ||
+ !Array.isArray(design.body?.rows) ||
+ !design.body.values ||
+ typeof design.body.values !== 'object'
+ ) {
+ throw new Error('The saved design has an unsupported format.');
+ }
+ return design;
+ } catch (error) {
+ // Discard only invalid demo data; a storage read failure never reaches here.
+ try {
+ storage.removeItem(STORAGE_KEY);
+ } catch {
+ // Storage may also block removal. The caller still loads the React template.
+ }
+ throw error;
}
- return design;
}
export function saveDesign(storage: Storage, design: EmailDesign): void {
diff --git a/demo/test/elements.test.tsx b/demo/test/elements.test.tsx
index 9439868dc..b74d3ef2a 100644
--- a/demo/test/elements.test.tsx
+++ b/demo/test/elements.test.tsx
@@ -11,7 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { EmailEditorProps } from 'react-email-editor';
import ElementsExample from '../src/elements';
import { createWelcomeDesign } from '../src/elements/welcome';
-import { STORAGE_KEY } from '../src/elements/storage';
+import { STORAGE_KEY, type Editor } from '../src/elements/storage';
const harness = vi.hoisted(() => ({
props: null as EmailEditorProps | null,
@@ -66,6 +66,7 @@ beforeEach(() => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
+ vi.useRealTimers();
});
describe('Elements integration', () => {
@@ -146,16 +147,130 @@ describe('Elements integration', () => {
expect(harness.loadDesign).toHaveBeenLastCalledWith(edited);
});
- it('recovers from malformed saved data with a visible warning', () => {
+ it.each(['{broken', JSON.stringify({ version: 1, design: {} })])(
+ 'clears invalid saved data and only warns once: %s',
+ (saved) => {
+ window.localStorage.setItem(STORAGE_KEY, saved);
+ window.localStorage.setItem('other-design', 'keep me');
+ const first = mount();
+ signalReady();
+ expect(screen.getByRole('alert').textContent).toContain('Could not read');
+ expect(JSON.stringify(harness.loadDesign.mock.calls[0][0])).toContain(
+ 'Welcome to Acme'
+ );
+ expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
+ expect(window.localStorage.getItem('other-design')).toBe('keep me');
+ first.unmount();
+ mount();
+ signalReady();
+ expect(screen.queryByRole('alert')).toBeNull();
+ }
+ );
+
+ it('still loads the template when removing corrupt data is blocked', () => {
window.localStorage.setItem(STORAGE_KEY, '{broken');
+ vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
+ throw new Error('Storage blocked');
+ });
+ mount();
+ signalReady();
+ expect(screen.getByRole('alert').textContent).toContain('Could not read');
+ expect(JSON.stringify(harness.loadDesign.mock.calls[0][0])).toContain(
+ 'Welcome to Acme'
+ );
+ });
+
+ it('does not discard saved data when reading storage is blocked', () => {
+ vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
+ throw new Error('Storage blocked');
+ });
+ const remove = vi.spyOn(Storage.prototype, 'removeItem');
mount();
signalReady();
+ expect(remove).not.toHaveBeenCalled();
expect(screen.getByRole('alert').textContent).toContain('Could not read');
expect(JSON.stringify(harness.loadDesign.mock.calls[0][0])).toContain(
'Welcome to Acme'
);
});
+ it('times out, retries, and ignores the old callback during the retry', () => {
+ vi.useFakeTimers();
+ const callbacks: Parameters[0][] = [];
+ harness.exportHtml.mockImplementation((callback) =>
+ callbacks.push(callback)
+ );
+ mount();
+ signalReady();
+ act(() => harness.listeners['design:loaded']());
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ expect(
+ (screen.getByRole('button', { name: 'Exporting…' }) as HTMLButtonElement)
+ .disabled
+ ).toBe(true);
+ act(() => vi.advanceTimersByTime(30_000));
+ expect(screen.getByRole('alert').textContent).toContain('Export timed out');
+ expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ const design = createWelcomeDesign();
+ act(() =>
+ callbacks[0]({ design, html: 'Stale export', chunks: {} } as Parameters<
+ (typeof callbacks)[0]
+ >[0])
+ );
+ expect(
+ (screen.getByRole('button', { name: 'Exporting…' }) as HTMLButtonElement)
+ .disabled
+ ).toBe(true);
+ expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
+ act(() =>
+ callbacks[1]({ design, html: 'Latest export', chunks: {} } as Parameters<
+ (typeof callbacks)[1]
+ >[0])
+ );
+ expect(
+ (
+ screen.getByRole('textbox', {
+ name: 'Exported output',
+ }) as HTMLTextAreaElement
+ ).value
+ ).toBe('Latest export');
+ expect(
+ JSON.parse(window.localStorage.getItem(STORAGE_KEY)!).design
+ ).toEqual(design);
+ act(() => vi.advanceTimersByTime(30_000));
+ expect(screen.queryByRole('alert')).toBeNull();
+ expect(
+ (
+ screen.getByRole('button', {
+ name: 'Save and export',
+ }) as HTMLButtonElement
+ ).disabled
+ ).toBe(false);
+ });
+
+ it('cancels an outstanding export on unmount', () => {
+ vi.useFakeTimers();
+ let callback!: Parameters[0];
+ harness.exportHtml.mockImplementation((value) => {
+ callback = value;
+ });
+ const view = mount();
+ signalReady();
+ act(() => harness.listeners['design:loaded']());
+ fireEvent.click(screen.getByRole('button', { name: 'Save and export' }));
+ view.unmount();
+ expect(vi.getTimerCount()).toBe(0);
+ act(() =>
+ callback({
+ design: createWelcomeDesign(),
+ html: 'Late export',
+ chunks: {},
+ } as Parameters[0])
+ );
+ expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull();
+ });
+
it('allows retrying after an export error', () => {
mount();
signalReady();
diff --git a/demo/test/setup.ts b/demo/test/setup.ts
new file mode 100644
index 000000000..a7842b33a
--- /dev/null
+++ b/demo/test/setup.ts
@@ -0,0 +1,8 @@
+import type {} from 'vitest/jsdom';
+
+// Use browser storage from jsdom, not Node 25's native localStorage global.
+Object.defineProperty(globalThis, 'localStorage', {
+ configurable: true,
+ writable: true,
+ value: jsdom.window.localStorage,
+});
diff --git a/demo/vitest.config.ts b/demo/vitest.config.ts
index 0464ce293..1fb0007d0 100644
--- a/demo/vitest.config.ts
+++ b/demo/vitest.config.ts
@@ -4,5 +4,9 @@ import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: { dedupe: ['react', 'react-dom'] },
- test: { environment: 'jsdom', include: ['test/*.test.tsx'] },
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./test/setup.ts'],
+ include: ['test/*.test.tsx'],
+ },
});
From b944e1b41ec7479c8236237c3778da95748e27e0 Mon Sep 17 00:00:00 2001
From: Adeel Raza
Date: Sun, 4 Oct 2026 10:34:21 +0400
Subject: [PATCH 4/5] ci: scope demo checks and validate marked documentation
examples
---
.github/workflows/ci.yml | 19 --------
.github/workflows/demo.yml | 54 +++++++++++++++++++++++
demo/scripts/check-docs.mjs | 87 ++++++++++++++++++++++---------------
3 files changed, 105 insertions(+), 55 deletions(-)
create mode 100644 .github/workflows/demo.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 39c689dfe..ba9a8b25e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,25 +26,6 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- demo:
- name: Elements demo and documentation examples
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v7
- - uses: actions/setup-node@v7
- with:
- node-version: 22
- cache: npm
- cache-dependency-path: |
- package-lock.json
- demo/package-lock.json
- - run: npm ci
- - run: npm run build
- - run: npm --prefix demo ci
- - run: npm --prefix demo run build
- - run: npm --prefix demo test
- - run: npm --prefix demo run test:docs
-
test:
name: Test (Node ${{ matrix.node }}, React ${{ matrix.react }})
runs-on: ubuntu-latest
diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml
new file mode 100644
index 000000000..6f01b8b1c
--- /dev/null
+++ b/.github/workflows/demo.yml
@@ -0,0 +1,54 @@
+name: Demo and documentation
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - 'demo/**'
+ - 'docs/**'
+ - 'src/**'
+ - 'README.md'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'tsconfig.json'
+ - 'tsup.config.ts'
+ - '.nvmrc'
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/demo.yml'
+ pull_request:
+ paths:
+ - 'demo/**'
+ - 'docs/**'
+ - 'src/**'
+ - 'README.md'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'tsconfig.json'
+ - 'tsup.config.ts'
+ - '.nvmrc'
+ - '.github/workflows/ci.yml'
+ - '.github/workflows/demo.yml'
+
+jobs:
+ demo:
+ name: Demo and docs (Node ${{ matrix.node }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [22, 25]
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: ${{ matrix.node }}
+ cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ demo/package-lock.json
+ # The root prepare script builds the wrapper during installation.
+ - run: npm ci
+ - run: npm --prefix demo ci
+ - run: npm --prefix demo run build
+ - run: npm --prefix demo test
+ - run: npm --prefix demo run test:docs
diff --git a/demo/scripts/check-docs.mjs b/demo/scripts/check-docs.mjs
index a72d8d896..6c73f4a9b 100644
--- a/demo/scripts/check-docs.mjs
+++ b/demo/scripts/check-docs.mjs
@@ -1,49 +1,64 @@
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import { spawnSync } from 'node:child_process';
+import ts from 'typescript';
const demo = fileURLToPath(new URL('..', import.meta.url));
const root = resolve(demo, '..');
const temporary = await mkdtemp(join(demo, 'node_modules', '.docs-check-'));
try {
- const paths = [];
- for (const [name, source] of [
- ['readme', 'README.md'],
- ['elements', 'docs/react-elements.md'],
- ]) {
+ const examples = new Map();
+ for (const source of ['README.md', 'docs/react-elements.md']) {
const markdown = await readFile(join(root, source), 'utf8');
- const blocks = [...markdown.matchAll(/```tsx\n([\s\S]*?)```/g)];
- if (blocks.length !== 1)
- throw new Error(`Expected one complete TSX example in ${source}`);
- const path = join(temporary, `${name}.tsx`);
- await writeFile(path, blocks[0][1]);
- paths.push(path);
+ const blocks = [
+ ...markdown.matchAll(/\s*```tsx\r?\n([\s\S]*?)^```/gm),
+ ];
+ if (!blocks.length)
+ throw new Error(
+ `${source}: no TSX examples marked with `
+ );
+ for (const [index, block] of blocks.entries()) {
+ if (!block[1].trim())
+ throw new Error(`${source}: marked example ${index + 1} is empty`);
+ const path = join(temporary, `${examples.size}.tsx`);
+ const offset = block.index + block[0].indexOf(block[1]);
+ examples.set(path, {
+ source,
+ line: markdown.slice(0, offset).split('\n').length,
+ });
+ await writeFile(path, block[1]);
+ }
}
- const result = spawnSync(
- process.execPath,
- [
- join(demo, 'node_modules/typescript/bin/tsc'),
- '--ignoreConfig',
- '--noEmit',
- '--strict',
- '--skipLibCheck',
- '--jsx',
- 'react-jsx',
- '--target',
- 'ES2020',
- '--module',
- 'ESNext',
- '--moduleResolution',
- 'bundler',
- '--verbatimModuleSyntax',
- ...paths,
- ],
- { stdio: 'inherit' }
- );
- if (result.error) throw result.error;
- if (result.status !== 0) throw new Error('Documentation typecheck failed');
- console.log('Both documentation examples pass strict TypeScript checks.');
+ const program = ts.createProgram([...examples.keys()], {
+ noEmit: true,
+ strict: true,
+ skipLibCheck: true,
+ jsx: ts.JsxEmit.ReactJSX,
+ target: ts.ScriptTarget.ES2020,
+ module: ts.ModuleKind.ESNext,
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
+ moduleDetection: ts.ModuleDetectionKind.Force,
+ verbatimModuleSyntax: true,
+ });
+ const diagnostics = ts.getPreEmitDiagnostics(program);
+ for (const diagnostic of diagnostics) {
+ let location = '';
+ if (diagnostic.file && diagnostic.start !== undefined) {
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
+ diagnostic.start
+ );
+ const example = examples.get(diagnostic.file.fileName);
+ location = `${example?.source ?? diagnostic.file.fileName}:${(example?.line ?? 1) + line}:${character + 1}: `;
+ }
+ console.error(
+ `${location}TS${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
+ );
+ }
+ if (diagnostics.length) process.exitCode = 1;
+ else
+ console.log(
+ `All ${examples.size} marked documentation examples pass strict TypeScript checks.`
+ );
} finally {
await rm(temporary, { recursive: true, force: true });
}
From dacf25cc26cf7ecaceec385169f11115a9b4303a Mon Sep 17 00:00:00 2001
From: Adeel Raza
Date: Sun, 4 Oct 2026 10:55:50 +0400
Subject: [PATCH 5/5] docs: make README Elements guidance self-contained
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 940dbc41b..245ed5380 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,7 @@ You can submit new language translations by creating a PR on this GitHub repo: h
## Guides and related workflows
-Writing templates in React? See the [Elements example](./docs/react-elements.md): generate design JSON with `@unlayer/react-elements` and edit it here.
+For React-authored email templates, including templates created by coding agents, use `@unlayer/react-elements`. Generate design JSON with `renderToJson()`, then pass it to React Email Editor’s `loadDesign()` for visual editing, saving, and HTML export. See the [complete example](https://github.com/unlayer/react-email-editor/blob/master/docs/react-elements.md).
### License