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/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 6f292acc7..245ed5380 100644
--- a/README.md
+++ b/README.md
@@ -53,41 +53,44 @@ 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.
+ // 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 +98,8 @@ const App = (props) => {
createRoot(document.getElementById('app')!).render( );
```
+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.
### Next.js / React Server Components
@@ -145,9 +150,9 @@ 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
-- [`@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.
+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
diff --git a/context7.json b/context7.json
index 8098531b4..456129817 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()."
+ "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
new file mode 100644
index 000000000..dcb514dfd
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,40 @@
+# React Email Editor demo
+
+Run from the repository root with Node.js 22.12+:
+
+```bash
+npm ci
+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:..`; 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.
+
+## 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 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/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..6c73f4a9b
--- /dev/null
+++ b/demo/scripts/check-docs.mjs
@@ -0,0 +1,64 @@
+import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
+import { join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+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 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(/\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 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 });
+}
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..ece5103ee
--- /dev/null
+++ b/demo/src/elements/index.tsx
@@ -0,0 +1,213 @@
+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 pendingExport = useRef<(() => boolean) | null>(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]);
+
+ useEffect(
+ () => () => {
+ pendingExport.current?.();
+ },
+ []
+ );
+
+ 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 || 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);
+ 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 {
+ if (!finish()) return;
+ 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..610525a73
--- /dev/null
+++ b/demo/src/elements/storage.ts
@@ -0,0 +1,39 @@
+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;
+ 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;
+ }
+}
+
+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..1f353d2fb
--- /dev/null
+++ b/demo/src/elements/welcome.tsx
@@ -0,0 +1,38 @@
+import {
+ Email,
+ Row,
+ Column,
+ Heading,
+ Paragraph,
+ Button,
+ renderToJson,
+} from '@unlayer/react-elements';
+import type { EmailDesign } from './storage';
+
+export function createWelcomeDesign() {
+ return renderToJson(
+
+
+
+
+ Welcome to Acme
+
+
+
+ Open your workspace
+
+
+
+
+
+ ) as EmailDesign; // Bridge Elements 0.1.22 design types to the editor.
+}
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..b74d3ef2a
--- /dev/null
+++ b/demo/test/elements.test.tsx
@@ -0,0 +1,319 @@
+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, type Editor } 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();
+ vi.useRealTimers();
+});
+
+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.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();
+ 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/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/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..1fb0007d0
--- /dev/null
+++ b/demo/vitest.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: { dedupe: ['react', 'react-dom'] },
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./test/setup.ts'],
+ include: ['test/*.test.tsx'],
+ },
+});
diff --git a/docs/react-elements.md b/docs/react-elements.md
new file mode 100644
index 000000000..9511b74ee
--- /dev/null
+++ b/docs/react-elements.md
@@ -0,0 +1,122 @@
+# 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 design types to the editor.
+}
+
+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 --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.