diff --git a/.changeset/brave-peaches-bake.md b/.changeset/brave-peaches-bake.md
new file mode 100644
index 000000000..25026f910
--- /dev/null
+++ b/.changeset/brave-peaches-bake.md
@@ -0,0 +1,32 @@
+---
+'@portabletext/editor': major
+---
+
+feat!: remove `PortableTextEditor` React component
+
+## Migration
+
+If you were using `PortableTextEditor` as a React component, switch to `EditorProvider`:
+
+```diff
+- import {PortableTextEditor} from '@portabletext/editor'
++ import {EditorProvider} from '@portabletext/editor'
+
+-
++
+
+-
++
+```
+
+The `PortableTextEditorProps` type export has also been removed.
diff --git a/.changeset/config.json b/.changeset/config.json
index cdb41b19a..ff893ba45 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -8,7 +8,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": ["docs", "playground", "example-basic", "example-legacy"],
+ "ignore": ["docs", "playground", "example-basic"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"updateInternalDependents": "always",
"onlyUpdatePeerDependentsWhenOutOfRange": true
diff --git a/examples/legacy/.gitignore b/examples/legacy/.gitignore
deleted file mode 100644
index a547bf36d..000000000
--- a/examples/legacy/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/examples/legacy/README.md b/examples/legacy/README.md
deleted file mode 100644
index e273b625a..000000000
--- a/examples/legacy/README.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
-
-- Configure the top-level `parserOptions` property like this:
-
-```js
-export default tseslint.config({
- languageOptions: {
- // other options...
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- },
-})
-```
-
-- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
-- Optionally add `...tseslint.configs.stylisticTypeChecked`
-- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
-
-```js
-// eslint.config.js
-import react from 'eslint-plugin-react'
-
-export default tseslint.config({
- // Set the react version
- settings: {react: {version: '18.3'}},
- plugins: {
- // Add the react plugin
- react,
- },
- rules: {
- // other rules...
- // Enable its recommended rules
- ...react.configs.recommended.rules,
- ...react.configs['jsx-runtime'].rules,
- },
-})
-```
diff --git a/examples/legacy/eslint.config.js b/examples/legacy/eslint.config.js
deleted file mode 100644
index 62ee28334..000000000
--- a/examples/legacy/eslint.config.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import js from '@eslint/js'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import globals from 'globals'
-import tseslint from 'typescript-eslint'
-
-export default tseslint.config(
- {ignores: ['dist']},
- reactHooks.configs.flat.recommended,
- {
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- plugins: {
- 'react-refresh': reactRefresh,
- },
- rules: {
- 'react-hooks/exhaustive-deps': 'error',
- 'react-refresh/only-export-components': [
- 'warn',
- {allowConstantExport: true},
- ],
- },
- },
-)
diff --git a/examples/legacy/index.html b/examples/legacy/index.html
deleted file mode 100644
index e4b78eae1..000000000
--- a/examples/legacy/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Vite + React + TS
-
-
-
-
-
-
diff --git a/examples/legacy/package.json b/examples/legacy/package.json
deleted file mode 100644
index cfd5fdb49..000000000
--- a/examples/legacy/package.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "name": "example-legacy",
- "version": "0.0.0",
- "private": true,
- "type": "module",
- "scripts": {
- "build": "tsc -b && vite build",
- "check:lint": "eslint .",
- "check:types": "tsc",
- "clean": "del .turbo && del dist && del node_modules",
- "dev": "vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "@portabletext/editor": "workspace:*",
- "@portabletext/patches": "workspace:*",
- "@sanity/schema": "^4.0.1",
- "@xstate/react": "^4.1.3",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "rxjs": "^7.8.1",
- "xstate": "^5.18.2"
- },
- "devDependencies": {
- "@eslint/js": "^9.34.0",
- "@sanity/types": "^4.0.1",
- "@types/react": "^19.2.2",
- "@types/react-dom": "^19.2.2",
- "@vitejs/plugin-react": "^4.3.3",
- "babel-plugin-react-compiler": "^1.0.0",
- "eslint": "^9.38.0",
- "eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.4.14",
- "globals": "^15.11.0",
- "typescript": "catalog:",
- "typescript-eslint": "^8.46.1",
- "vite": "^5.4.10"
- }
-}
diff --git a/examples/legacy/public/vite.svg b/examples/legacy/public/vite.svg
deleted file mode 100644
index e7b8dfb1b..000000000
--- a/examples/legacy/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/legacy/src/App.tsx b/examples/legacy/src/App.tsx
deleted file mode 100644
index d3a7be235..000000000
--- a/examples/legacy/src/App.tsx
+++ /dev/null
@@ -1,407 +0,0 @@
-import {
- MutationChange,
- Patch,
- PortableTextBlock,
- PortableTextChild,
- PortableTextEditable,
- PortableTextEditor,
- RenderAnnotationFunction,
- RenderBlockFunction,
- RenderChildFunction,
- RenderDecoratorFunction,
- RenderStyleFunction,
- usePortableTextEditor,
- usePortableTextEditorSelection,
-} from '@portabletext/editor'
-import {applyAll} from '@portabletext/patches'
-import {useActorRef, useSelector} from '@xstate/react'
-import {useEffect, useMemo} from 'react'
-import {Subject} from 'rxjs'
-import {ActorRefFrom, assign, emit, sendParent, setup} from 'xstate'
-import './editor.css'
-import {schema} from './schema'
-
-const editorMachine = setup({
- types: {
- events: {} as
- | MutationChange
- | {
- type: 'patches'
- patches: MutationChange['patches']
- snapshot: MutationChange['snapshot']
- },
- emitted: {} as {
- type: 'patches'
- patches: MutationChange['patches']
- snapshot: MutationChange['snapshot']
- },
- },
-}).createMachine({
- id: 'editor',
- on: {
- mutation: {
- actions: sendParent(({event, self}) => ({
- ...event,
- type: 'editor.mutation',
- editorId: self.id,
- })),
- },
- patches: {
- actions: emit(({event}) => event),
- },
- },
-})
-
-const playgroundMachine = setup({
- types: {
- context: {} as {
- editors: Array>
- value: Array | undefined
- },
- events: {} as {
- type: 'editor.mutation'
- editorId: ActorRefFrom['id']
- patches: Array
- snapshot: Array | undefined
- },
- },
- actors: {
- 'editor machine': editorMachine,
- },
-}).createMachine({
- id: 'playground',
- context: ({spawn}) => ({
- editors: [spawn(editorMachine), spawn(editorMachine)],
- value: undefined,
- }),
- on: {
- 'editor.mutation': {
- actions: [
- ({context, event}) => {
- for (const editor of context.editors) {
- editor.send({
- type: 'patches',
- patches: event.patches.map((patch) => ({
- ...patch,
- origin: event.editorId === editor.id ? 'local' : 'remote',
- })),
- snapshot: event.snapshot,
- })
- }
- },
- assign({
- value: ({context, event}) => {
- return applyAll(context.value, event.patches)
- },
- }),
- ],
- },
- },
-})
-
-/**
- * WARNING: This is just an example app to make sure that legacy APIs are still
- * working. Don't use this as a reference for your own project.
- */
-function App() {
- const playground = useActorRef(playgroundMachine)
- const editors = useSelector(playground, (s) => s.context.editors)
- const value = useSelector(playground, (s) => s.context.value)
-
- return (
- <>
- {editors.map((editor) => (
-
- ))}
-
- {JSON.stringify(value, null, 2)}
-
- >
- )
-}
-
-function Editor(props: {
- editorRef: ActorRefFrom
- value: Array | undefined
-}) {
- const patches$ = useMemo(
- () =>
- new Subject<{
- patches: Patch[]
- snapshot: PortableTextBlock[] | undefined
- }>(),
- [],
- )
- useEffect(() => {
- props.editorRef.on('patches', (event) => {
- patches$.next({
- patches: event.patches,
- snapshot: event.snapshot,
- })
- })
- }, [props.editorRef, patches$])
-
- return (
-
-
{
- if (change.type === 'mutation') {
- props.editorRef.send(change)
- }
- }}
- >
-
- {
- /**
- * In a real-world scenario you would want to trigger a dialog
- * here so you can ask the user to input the URL for the link.
- */
- if (PortableTextEditor.isAnnotationActive(editor, 'link')) {
- PortableTextEditor.removeAnnotation(editor, {name: 'link'})
- } else {
- PortableTextEditor.addAnnotation(
- editor,
- {name: 'link'},
- {href: 'https://example.com'},
- )
- }
- },
- },
- }}
- style={{border: '1px solid black', padding: '0.5em'}}
- renderDecorator={renderDecorator}
- renderAnnotation={renderAnnotation}
- renderBlock={renderBlock}
- renderStyle={renderStyle}
- renderChild={renderChild}
- renderListItem={(props) => <>{props.children}>}
- />
-
-
- )
-}
-
-const renderDecorator: RenderDecoratorFunction = (props) => {
- if (props.value === 'strong') {
- return {props.children}
- }
- if (props.value === 'em') {
- return {props.children}
- }
- if (props.value === 'underline') {
- return {props.children}
- }
- return <>{props.children}>
-}
-
-const renderAnnotation: RenderAnnotationFunction = (props) => {
- if (props.schemaType.name === 'link') {
- return {props.children}
- }
-
- return <>{props.children}>
-}
-
-const renderBlock: RenderBlockFunction = (props) => {
- if (props.schemaType.name === 'image' && isImage(props.value)) {
- return (
-
- IMG: {props.value.src}
-
- )
- }
-
- return {props.children}
-}
-
-function isImage(
- props: PortableTextBlock,
-): props is PortableTextBlock & {src: string} {
- return 'src' in props
-}
-
-const renderStyle: RenderStyleFunction = (props) => {
- if (props.schemaType.value === 'h1') {
- return {props.children}
- }
- if (props.schemaType.value === 'h2') {
- return {props.children}
- }
- if (props.schemaType.value === 'h3') {
- return {props.children}
- }
- if (props.schemaType.value === 'blockquote') {
- return {props.children}
- }
- return <>{props.children}>
-}
-
-const renderChild: RenderChildFunction = (props) => {
- if (props.schemaType.name === 'stock-ticker' && isStockTicker(props.value)) {
- return (
-
- {props.value.symbol}
-
- )
- }
-
- return <>{props.children}>
-}
-
-function isStockTicker(
- props: PortableTextChild,
-): props is PortableTextChild & {symbol: string} {
- return 'symbol' in props
-}
-
-function Toolbar() {
- const editor = usePortableTextEditor()
- usePortableTextEditorSelection()
-
- const decoratorButtons = editor.schemaTypes.decorators.map((decorator) => (
-
- ))
-
- const linkButton = (
-
- )
-
- const styleButtons = editor.schemaTypes.styles.map((style) => (
-
- ))
-
- const listButtons = editor.schemaTypes.lists.map((list) => (
-
- ))
-
- const imageButton = (
-
- )
-
- const stockTickerButton = (
-
- )
-
- return (
- <>
- {decoratorButtons}
- {linkButton}
- {styleButtons}
- {listButtons}
- {imageButton}
- {stockTickerButton}
- >
- )
-}
-
-function DecoratorButton(props: {decorator: string}) {
- const editor = usePortableTextEditor()
- usePortableTextEditorSelection()
-
- return (
-
- )
-}
-
-export default App
diff --git a/examples/legacy/src/editor.css b/examples/legacy/src/editor.css
deleted file mode 100644
index 62d721236..000000000
--- a/examples/legacy/src/editor.css
+++ /dev/null
@@ -1,187 +0,0 @@
-[role='textbox'] {
- --list-padding: 1em;
- /**
- * First, we initialize a counter for each block level.
- */
- counter-reset: level-1 level-2 level-3 level-4 level-5 level-6 level-7 level-8
- level-9 level-10;
-}
-
-[data-list-item] {
- display: flex;
- gap: 0.5rem;
-}
-[data-list-item]::before {
- text-align: right;
- width: 1.5rem;
-}
-
-[data-list-item='number'] {
- align-items: baseline;
-}
-[data-list-item='number']::before {
- font-size: 0.9rem;
- font-variant-numeric: tabular-nums;
-}
-
-[data-list-item='bullet'] {
- align-items: center;
-}
-[data-list-item='bullet']::before {
- font-size: 0.4rem;
-}
-
-/**
- * Then, the counter for each level is manually set to 1 whenever a list item
- * with index 1 is encountered.
- */
-[data-level='1'][data-list-index='1'] {
- counter-set: level-1 1;
-}
-[data-level='2'][data-list-index='1'] {
- counter-set: level-2 1;
-}
-[data-level='3'][data-list-index='1'] {
- counter-set: level-3 1;
-}
-[data-level='4'][data-list-index='1'] {
- counter-set: level-4 1;
-}
-[data-level='5'][data-list-index='1'] {
- counter-set: level-5 1;
-}
-[data-level='6'][data-list-index='1'] {
- counter-set: level-6 1;
-}
-[data-level='7'][data-list-index='1'] {
- counter-set: level-7 1;
-}
-[data-level='8'][data-list-index='1'] {
- counter-set: level-8 1;
-}
-[data-level='9'][data-list-index='1'] {
- counter-set: level-9 1;
-}
-[data-level='10'][data-list-index='1'] {
- counter-set: level-10 1;
-}
-
-/**
- * Thereafter, the count for each list level is incremented for each index
- * greater than 1.
- */
-[data-level='1']:not([data-list-index='1']) {
- counter-increment: level-1;
-}
-[data-level='2']:not([data-list-index='1']) {
- counter-increment: level-2;
-}
-[data-level='3']:not([data-list-index='1']) {
- counter-increment: level-3;
-}
-[data-level='4']:not([data-list-index='1']) {
- counter-increment: level-4;
-}
-[data-level='5']:not([data-list-index='1']) {
- counter-increment: level-5;
-}
-[data-level='6']:not([data-list-index='1']) {
- counter-increment: level-6;
-}
-[data-level='7']:not([data-list-index='1']) {
- counter-increment: level-7;
-}
-[data-level='8']:not([data-list-index='1']) {
- counter-increment: level-8;
-}
-[data-level='9']:not([data-list-index='1']) {
- counter-increment: level-9;
-}
-[data-level='10']:not([data-list-index='1']) {
- counter-increment: level-10;
-}
-
-/**
- * Finally, the calculated count is displayed in the list item.
- */
-[data-list-item='number'][data-level='1']::before {
- content: counter(level-1, decimal) '.';
-}
-[data-list-item='number'][data-level='2']::before {
- content: counter(level-2, lower-alpha) '.';
-}
-[data-list-item='number'][data-level='3']::before {
- content: counter(level-3, lower-roman) '.';
-}
-[data-list-item='number'][data-level='4']::before {
- content: counter(level-4, decimal) '.';
-}
-[data-list-item='number'][data-level='5']::before {
- content: counter(level-5, lower-alpha) '.';
-}
-[data-list-item='number'][data-level='6']::before {
- content: counter(level-6, lower-roman) '.';
-}
-[data-list-item='number'][data-level='7']::before {
- content: counter(level-7, decimal) '.';
-}
-[data-list-item='number'][data-level='8']::before {
- content: counter(level-8, lower-alpha) '.';
-}
-[data-list-item='number'][data-level='9']::before {
- content: counter(level-9, lower-roman) '.';
-}
-[data-list-item='number'][data-level='10']::before {
- content: counter(level-10, decimal) '.';
-}
-
-/**
- * Visual display of bulleted list items
- */
-[data-list-item='bullet'][data-level='1']::before,
-[data-list-item='bullet'][data-level='4']::before,
-[data-list-item='bullet'][data-level='7']::before,
-[data-list-item='bullet'][data-level='10']::before {
- content: '●';
-}
-[data-list-item='bullet'][data-level='2']::before,
-[data-list-item='bullet'][data-level='5']::before,
-[data-list-item='bullet'][data-level='8']::before {
- content: '○';
-}
-[data-list-item='bullet'][data-level='3']::before,
-[data-list-item='bullet'][data-level='6']::before,
-[data-list-item='bullet'][data-level='9']::before {
- content: '■';
-}
-
-/**
- * Padding for each level of list item
- */
-[data-level='2'] {
- padding-left: calc(var(--list-padding));
-}
-[data-level='3'] {
- padding-left: calc(var(--list-padding) * 2);
-}
-[data-level='4'] {
- padding-left: calc(var(--list-padding) * 3);
-}
-[data-level='5'] {
- padding-left: calc(var(--list-padding) * 4);
-}
-[data-level='6'] {
- padding-left: calc(var(--list-padding) * 5);
-}
-[data-level='7'] {
- padding-left: calc(var(--list-padding) * 6);
-}
-[data-level='8'] {
- padding-left: calc(var(--list-padding) * 7);
-}
-[data-level='9'] {
- padding-left: calc(var(--list-padding) * 8);
-}
-[data-level='10'] {
- padding-left: calc(var(--list-padding) * 9);
-}
diff --git a/examples/legacy/src/main.tsx b/examples/legacy/src/main.tsx
deleted file mode 100644
index 551f4cbe5..000000000
--- a/examples/legacy/src/main.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import {StrictMode} from 'react'
-import {createRoot} from 'react-dom/client'
-import App from './App.tsx'
-
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
diff --git a/examples/legacy/src/schema.ts b/examples/legacy/src/schema.ts
deleted file mode 100644
index fed114a90..000000000
--- a/examples/legacy/src/schema.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {Schema} from '@sanity/schema'
-import {defineField, defineType} from '@sanity/types'
-
-const imageType = defineType({
- name: 'custom image',
- title: 'Image',
- type: 'object',
- fields: [
- defineField({
- name: 'url',
- type: 'string',
- }),
- ],
-})
-
-const stockTickerType = defineType({
- name: 'stock-ticker',
- type: 'object',
- fields: [
- defineField({
- name: 'symbol',
- type: 'string',
- }),
- ],
-})
-
-const portableTextType = defineType({
- type: 'array',
- name: 'body',
- of: [
- {
- type: 'block',
- name: 'block',
- styles: [
- {title: 'Normal', value: 'normal'},
- {title: 'H1', value: 'h1'},
- {title: 'H2', value: 'h2'},
- {title: 'H3', value: 'h3'},
- {title: 'H4', value: 'h4'},
- {title: 'H5', value: 'h5'},
- {title: 'H6', value: 'h6'},
- {title: 'Quote', value: 'blockquote'},
- ],
- marks: {
- annotations: [
- {
- name: 'comment',
- type: 'object',
- fields: [{type: 'string', name: 'text'}],
- },
- {
- name: 'link',
- type: 'object',
- fields: [{type: 'string', name: 'href'}],
- },
- ],
- },
- of: [{type: 'stock-ticker'}],
- },
- {type: 'custom image'},
- ],
-})
-
-export const schema = Schema.compile({
- types: [portableTextType, imageType, stockTickerType],
-}).get('body')
diff --git a/examples/legacy/src/vite-env.d.ts b/examples/legacy/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2a..000000000
--- a/examples/legacy/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/examples/legacy/tsconfig.app.json b/examples/legacy/tsconfig.app.json
deleted file mode 100644
index f867de0dd..000000000
--- a/examples/legacy/tsconfig.app.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "Bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["src"]
-}
diff --git a/examples/legacy/tsconfig.json b/examples/legacy/tsconfig.json
deleted file mode 100644
index b3de6fde9..000000000
--- a/examples/legacy/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "files": [],
- "references": [
- {"path": "./tsconfig.app.json"},
- {"path": "./tsconfig.node.json"}
- ]
-}
diff --git a/examples/legacy/tsconfig.node.json b/examples/legacy/tsconfig.node.json
deleted file mode 100644
index abcd7f0da..000000000
--- a/examples/legacy/tsconfig.node.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "Bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/examples/legacy/vite.config.ts b/examples/legacy/vite.config.ts
deleted file mode 100644
index 87764ac2f..000000000
--- a/examples/legacy/vite.config.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import path from 'node:path'
-import react from '@vitejs/plugin-react'
-import {defineConfig} from 'vite'
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [
- react({
- babel: {plugins: [['babel-plugin-react-compiler', {target: '19'}]]},
- }),
- ],
- resolve: {
- alias: {
- '@portabletext/editor': path.resolve(
- __dirname,
- '../../packages/editor/src',
- ),
- '@portabletext/block-tools': path.resolve(
- __dirname,
- '../../packages/block-tools/src',
- ),
- '@portabletext/patches': path.resolve(
- __dirname,
- '../../packages/patches/src',
- ),
- },
- },
-})
diff --git a/package.json b/package.json
index 4bce69f68..f0cce63f3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,6 @@
"build:docs": "turbo build --filter=docs",
"build:editor": "turbo build --filter=@portabletext/editor...",
"build:example-basic": "turbo build --filter=example-basic...",
- "build:example-legacy": "turbo build --filter=example-legacy...",
"build:keyboard-shortcuts": "turbo build --filter=@portabletext/keyboard-shortcuts...",
"build:markdown": "turbo build --filter=@portabletext/markdown...",
"build:patches": "turbo build --filter=@portabletext/patches...",
@@ -29,7 +28,6 @@
"dev:docs": "turbo dev --filter=docs",
"dev:editor": "turbo dev --filter=@portabletext/editor...",
"dev:example-basic": "turbo dev --filter=example-basic",
- "dev:example-legacy": "turbo dev --filter=example-legacy",
"dev:keyboard-shortcuts": "turbo dev --filter=@portabletext/keyboard-shortcuts...",
"dev:playground": "turbo dev --filter=playground",
"dev:plugin-emoji-picker": "turbo dev --filter=@portabletext/plugin-emoji-picker...",
diff --git a/packages/editor/src/editor/PortableTextEditor.tsx b/packages/editor/src/editor/PortableTextEditor.tsx
index 06aa04a45..b256c1cab 100644
--- a/packages/editor/src/editor/PortableTextEditor.tsx
+++ b/packages/editor/src/editor/PortableTextEditor.tsx
@@ -3,107 +3,36 @@ import type {
PortableTextChild,
PortableTextObject,
} from '@portabletext/schema'
-import {
- Component,
- useEffect,
- type MutableRefObject,
- type PropsWithChildren,
-} from 'react'
import {Subject} from 'rxjs'
-import {Slate} from 'slate-react'
-import {stopActor} from '../internal-utils/stop-actor'
import type {
AddedAnnotationPaths,
EditableAPI,
EditableAPIDeleteOptions,
- EditorChange,
EditorChanges,
EditorSelection,
- PatchObservable,
PortableTextMemberSchemaTypes,
} from '../types/editor'
import type {Path} from '../types/paths'
-import type {ArrayDefinition, ArraySchemaType} from '../types/sanity-types'
-import {createInternalEditor, type InternalEditor} from './create-editor'
-import {EditorActorContext} from './editor-actor-context'
-import type {EditorActor} from './editor-machine'
-import {eventToChange} from './event-to-change'
-import type {MutationActor} from './mutation-machine'
-import {RelayActorContext} from './relay-actor-context'
-import type {RelayActor} from './relay-machine'
-import type {SyncActor} from './sync-machine'
-import {PortableTextEditorContext} from './usePortableTextEditor'
+import type {InternalEditor} from './create-editor'
/**
- * Props for the PortableTextEditor component
- *
- * @public
- * @deprecated Use `EditorProvider` instead
- */
-export type PortableTextEditorProps<
- TEditor extends InternalEditor | undefined = undefined,
-> = PropsWithChildren<
- TEditor extends InternalEditor
- ? {
- /**
- * @internal
- */
- editor: TEditor
- }
- : {
- editor?: undefined
-
- /**
- * Function that gets called when the editor changes the value
- */
- onChange: (change: EditorChange) => void
-
- /**
- * Schema type for the portable text field
- */
- schemaType: ArraySchemaType | ArrayDefinition
-
- /**
- * Function used to generate keys for array items (`_key`)
- */
- keyGenerator?: () => string
-
- /**
- * Observable of local and remote patches for the edited value.
- */
- patches$?: PatchObservable
-
- /**
- * Backward compatibility (renamed to patches$).
- */
- incomingPatches$?: PatchObservable
-
- /**
- * Whether or not the editor should be in read-only mode
- */
- readOnly?: boolean
-
- /**
- * The current value of the portable text field
- */
- value?: PortableTextBlock[]
-
- /**
- * A ref to the editor instance
- */
- editorRef?: MutableRefObject
- }
->
-
-/**
- * The main Portable Text Editor component.
* @public
- * @deprecated Use `EditorProvider` instead
+ * @deprecated Use `useEditor()` instead
+ *
+ * ```
+ * import {useEditor} from '@portabletext/editor'
+ *
+ * // Get the editor instance
+ * const editor = useEditor()
+ *
+ * // Send events to the editor
+ * editor.send(...)
+ *
+ * // Derive state from the editor
+ * const state = useEditorSelector(editor, snapshot => ...)
+ * ```
*/
-export class PortableTextEditor extends Component<
- PortableTextEditorProps
-> {
- public static displayName = 'PortableTextEditor'
+export class PortableTextEditor {
/**
* An observable of all the editor changes.
*/
@@ -121,120 +50,12 @@ export class PortableTextEditor extends Component<
*/
private editable: EditableAPI
- private actors?: {
- editorActor: EditorActor
- mutationActor: MutationActor
- relayActor: RelayActor
- syncActor: SyncActor
- }
-
- private subscriptions: Array<() => () => void> = []
- private unsubscribers: Array<() => void> = []
-
- constructor(props: PortableTextEditorProps) {
- super(props)
-
- if (props.editor) {
- this.editor = props.editor as InternalEditor
- this.schemaTypes = this.editor._internal.editorActor
- .getSnapshot()
- .context.getLegacySchema()
- } else {
- const {actors, editor, subscriptions} = createInternalEditor({
- initialValue: props.value,
- keyGenerator: props.keyGenerator,
- readOnly: props.readOnly,
- schema: props.schemaType,
- })
-
- this.subscriptions = subscriptions
- this.actors = actors
-
- this.editor = editor
- this.schemaTypes = actors.editorActor
- .getSnapshot()
- .context.getLegacySchema()
- }
-
- this.editable = this.editor._internal.editable
- }
-
- override componentDidMount(): void {
- if (!this.actors) {
- return
- }
-
- for (const subscription of this.subscriptions) {
- this.unsubscribers.push(subscription())
- }
-
- const relayActorSubscription = this.actors.relayActor.on('*', (event) => {
- const change = eventToChange(event)
-
- if (!change) {
- return
- }
-
- if (!this.props.editor) {
- this.props.onChange(change)
- }
-
- this.change$.next(change)
- })
-
- this.unsubscribers.push(relayActorSubscription.unsubscribe)
-
- this.actors.editorActor.start()
- this.actors.mutationActor.start()
- this.actors.relayActor.start()
- this.actors.syncActor.start()
- }
-
- override componentDidUpdate(prevProps: PortableTextEditorProps) {
- // Set up the schema type lookup table again if the source schema type changes
- if (
- !this.props.editor &&
- !prevProps.editor &&
- this.props.schemaType !== prevProps.schemaType
- ) {
- console.warn('Updating schema type is no longer supported')
- }
-
- if (!this.props.editor && !prevProps.editor) {
- if (this.props.readOnly !== prevProps.readOnly) {
- this.editor._internal.editorActor.send({
- type: 'update readOnly',
- readOnly: this.props.readOnly ?? false,
- })
- }
-
- if (this.props.value !== prevProps.value) {
- this.editor.send({
- type: 'update value',
- value: this.props.value,
- })
- }
-
- if (
- this.props.editorRef !== prevProps.editorRef &&
- this.props.editorRef
- ) {
- this.props.editorRef.current = this
- }
- }
- }
-
- override componentWillUnmount(): void {
- for (const unsubscribe of this.unsubscribers) {
- unsubscribe()
- }
-
- if (this.actors) {
- stopActor(this.actors.editorActor)
- stopActor(this.actors.mutationActor)
- stopActor(this.actors.relayActor)
- stopActor(this.actors.syncActor)
- }
+ constructor(config: {editor: InternalEditor}) {
+ this.editor = config.editor
+ this.schemaTypes = config.editor._internal.editorActor
+ .getSnapshot()
+ .context.getLegacySchema()
+ this.editable = config.editor._internal.editable
}
public setEditable = (editable: EditableAPI) => {
@@ -244,35 +65,6 @@ export class PortableTextEditor extends Component<
}
}
- override render() {
- const legacyPatches = !this.props.editor
- ? (this.props.incomingPatches$ ?? this.props.patches$)
- : undefined
-
- return (
- <>
- {legacyPatches ? (
-
- ) : null}
-
-
-
-
- {this.props.children}
-
-
-
-
- >
- )
- }
-
/**
* @deprecated
* Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/
@@ -757,23 +549,3 @@ export class PortableTextEditor extends Component<
return editor.editable?.isSelectionsOverlapping(selectionA, selectionB)
}
}
-
-function RoutePatchesObservableToEditorActor(props: {
- editorActor: EditorActor
- patches$: PatchObservable
-}) {
- useEffect(() => {
- const subscription = props.patches$.subscribe((payload) => {
- props.editorActor.send({
- type: 'patches',
- ...payload,
- })
- })
-
- return () => {
- subscription.unsubscribe()
- }
- }, [props.editorActor, props.patches$])
-
- return null
-}
diff --git a/packages/editor/src/editor/editor-provider.tsx b/packages/editor/src/editor/editor-provider.tsx
index c9cbe7ce4..a63005514 100644
--- a/packages/editor/src/editor/editor-provider.tsx
+++ b/packages/editor/src/editor/editor-provider.tsx
@@ -7,10 +7,7 @@ import {createInternalEditor} from './create-editor'
import {EditorActorContext} from './editor-actor-context'
import {EditorContext} from './editor-context'
import {eventToChange} from './event-to-change'
-import {
- PortableTextEditor,
- type PortableTextEditorProps,
-} from './PortableTextEditor'
+import {PortableTextEditor} from './PortableTextEditor'
import {RelayActorContext} from './relay-actor-context'
import {PortableTextEditorContext} from './usePortableTextEditor'
@@ -45,7 +42,7 @@ export function EditorProvider(props: EditorProviderProps) {
const internalEditor = createInternalEditor(props.initialConfig)
const portableTextEditor = new PortableTextEditor({
editor: internalEditor.editor,
- } as unknown as PortableTextEditorProps)
+ })
return {internalEditor, portableTextEditor}
})
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 3115e6573..0b3b440e4 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -41,7 +41,6 @@ export {usePortableTextEditor} from './editor/usePortableTextEditor'
export {usePortableTextEditorSelection} from './editor/usePortableTextEditorSelection'
export {defaultKeyGenerator as keyGenerator} from './utils/key-generator'
export {PortableTextEditor} from './editor/PortableTextEditor'
-export type {PortableTextEditorProps} from './editor/PortableTextEditor'
export type {EditorEmittedEvent, MutationEvent} from './editor/relay-machine'
export {useEditor} from './editor/use-editor'
export type {AddedAnnotationPaths} from './types/editor'